diff --git a/.gitignore b/.gitignore index eb91e715bbdc..2ea3a4839a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,13 @@ node_modules dist dist-* build +.history .vscode .publish _test-output cypress.zip tmp/ +.nyc_output # from extension Cached Theme.pak @@ -20,6 +22,9 @@ Cached Theme Material Design.pak # from https-proxy project packages/https-proxy/ca/ +# from desktop-gui +packages/desktop-gui/src/jsconfig.json + # from example packages/example/app packages/example/build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 777f84a55f79..054b593f3498 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,13 +13,14 @@ Thanks for taking the time to contribute! :smile: - [Report bugs](https://github.com/cypress-io/cypress/issues/new) by opening an issue. - [Request features](https://github.com/cypress-io/cypress/issues/new) by opening an issue. -- Write Code for one of our core packages. [Please thoroughly read our writing code guide](#writing-code). +- Write code for one of our core packages. [Please thoroughly read our writing code guide](#writing-code). ## Table of Contents - [CI Status](#ci-status) - [Code of Conduct](#code-of-conduct) -- [Bugs & Feature Requests](#bugs-features) +- [Opening Issues](#opening-issues) +- [Triaging Issues](#triaging-issues) - [Writing Documentation](#writing-documentation) - [Writing Code](#writing-code) - [What you need to know before getting started](#what-you-need-to-know-before-getting-started) @@ -52,7 +53,7 @@ Build status | Description All contributors are expecting to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). -## Bugs & Feature Requests +## Opening Issues **The most important things to do are:** @@ -84,7 +85,7 @@ For some issues, there are places you can check for more information. This may h When you file a feature request, we need you to **describe the problem you are facing first**, not just your desired solution. -Often, your problem may have a lot in common with other similar problems. If we understand your use case we can compare it to other use cases and sometimes find a more powerful or more general solution which solves several problems at once. Understanding the root issue can let us merge and contextualize things. Sometimes there's already a way to solve your problem that might just not be obvious. +Often, your problem may have a lot in common with other similar problems. If we understand your use case, we can compare it to other use cases and sometimes find a more powerful or more general solution which solves several problems at once. Understanding the root issue can let us merge and contextualize things. Sometimes there's already a way to solve your problem that might just not be obvious. Also, your proposed solution may not be compatible with the direction we want to take the product, but we may be able to come up with another solution which has approximately the same effect and does fit into the product direction. @@ -92,9 +93,35 @@ Also, your proposed solution may not be compatible with the direction we want to **It is nearly impossible for us to resolve many issues if we can not reproduce them. Your best chance of getting a bug looked at quickly is to provide a repository with a reproducible bug that can be cloned and run.** +## Triaging Issues + +When an issue is opened in [cypress](https://github.com/cypress-io/cypress), we need to evaluate the issue to determine what steps should be taken next. So, when approaching new issues, there are some steps that should be taken. + +### 1. Is this already an open issue? + +Search [all issues](https://github.com/cypress-io/cypress/issues) for keywords from the issue to ensure there isn't already an issue open for this. GitHub has some [search tips](https://help.github.com/articles/searching-issues-and-pull-requests/) that may help you better find the relevant issue. + +### 2. Is what they are describing actually happening? + +The best way to determine the validity of a bug is to recreate it yourself. Follow the directions or information provided to recreate the bug that is described. Did they provide a repository that demonstrates the bug? Great - fork it and run the project and steps required. If they did not provide a repository, the best way to reproduce the issue is to have a 'sandbox' project up and running locally for Cypress. This is just a simple project with Cypress installed where you can freely edit the application under test and the tests themselves to recreate the problem. + +**Attempting to recreate the bug will lead to a few scenarios:** + +#### 1. You can not recreate the bug + +Leave a comment on the issue saying, "I can't reproduce this situation with the code you provided. Could you provide more information or a repository demonstrating the bug?" + +#### 2. You can recreate the bug + +Leave a comment on the issue saying "I was able to reproduce this in Cypress version x.x.x" If you know where the code is that could possibly fix this issue - link to the file or line of code from the [cypress](https://github.com/cypress-io/cypress) repo and remind the user that we are open source and that we gladly accept PRs, even if they are a work in progress. + +#### 3. You can tell the problem is a user error + +In recreating the issue, you may realize that they had a typo or used the Cypress API incorrectly, etc. Leave a comment informing the user of their error and close the issue – or ask them to close the issue if it fixes their problem. + ## Writing Documentation -Cypress documentation lives in separate repository with its own dependencies and build tools. +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). ## Writing code @@ -153,14 +180,13 @@ npm run build npm start ``` -If there are errors building the packages, run with `DEBUG=cypress:*` -option to see more details. +If there are errors building the packages, prefix the commands with `DEBUG=cypress:*` to see more details. -This outputs a lot of debugging lines. To focus on an individual module run with `DEBUG=cypress:launcher` for instance. +This outputs a lot of debugging lines. To focus on an individual module, run with `DEBUG=cypress:launcher` for instance. -When running `npm start` this routes through the CLI and eventually calls `npm run dev` with the proper arguments. This enables Cypress day to day development to match the logic of the built binary + CLI integration. +When running `npm start` this routes through the CLI and eventually calls `npm run dev` with the proper arguments. This enables Cypress day-to-day development to match the logic of the built binary + CLI integration. -If you want to bypass the CLI entirely you can use the `npm run dev` task and pass arguments directly. +If you want to bypass the CLI entirely, you can use the `npm run dev` task and pass arguments directly. #### Tasks @@ -280,9 +306,9 @@ npm rebuild node-sass ### Packages -Generally when making contributions, you are typically making it to a small number of packages. Most of your local development work will be inside a single package at a time. +Generally when making contributions, you are typically making them to a small number of packages. Most of your local development work will be inside a single package at a time. -Each package documents how to best work with it, so simple consult the `README.md` of each package. +Each package documents how to best work with it, so simply consult the `README.md` of each package. They will outline development and test procedures. When in doubt just look at the `scripts` of each `package.json` file. Everything we do at Cypress is contained there. @@ -292,7 +318,7 @@ They will outline development and test procedures. When in doubt just look at th The repository is setup with two main (protected) branches. -- `master` is the code already published in the last Cypress version +- `master` is the code already published in the last Cypress version. - `develop` is the current latest "edge" code. This branch is set as the default branch, and all pull requests should be made against this branch. ### Pull Requests diff --git a/DEPLOY.md b/DEPLOY.md index 3cffdd526e7f..8634bb4623bf 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,14 +1,14 @@ ## Deployment -Anyone can build the binary and NPM package, but you can only deploy Cypress application -and publish NPM module `cypress` if you are a member of `cypress` NPM organization. +Anyone can build the binary and NPM package, but you can only deploy the Cypress application +and publish the NPM module `cypress` if you are a member of `cypress` NPM organization. -**important** see the [publishing](#publishing) section for how to build, test and publish a +> :warning: See the [publishing](#publishing) section for how to build, test and publish a new official version of the binary and `cypress` NPM package. ### Set next version on CIs -We build NPM package and binary on all major platforms (Linux, Mac, Windows) on different CI +We build the NPM package and binary on all major platforms (Linux, Mac, Windows) on different CI 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*. @@ -18,18 +18,22 @@ Use script command `npm run set-next-ci-version` to do this. Building a new NPM package is very quick. -- increment the version in the root `package.json` +- Increment the version in the root `package.json` - `cd cli && npm run build` -This builds the `cypress` NPM package, transpiles the code into ES5 version to be compatible -with the common Node versions and puts the result into `cli/build` folder. You could -publish from there, but first you need to build and upload the binary with *same version*; +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. + +You could publish from there, but first you need to build and upload the binary with the *same version*; this guarantees that when users do `npm i cypress@` they can download the binary -with same version `x.y.z` from Cypress CDN service. +with the same version `x.y.z` from Cypress CDN service. ### Building the binary -First, you need to build, zip and upload application binary to Cypress server. +First, you need to build, zip and upload the application binary to the Cypress server. You can use a single command to do all tasks at once: @@ -52,16 +56,16 @@ npm run binary-deploy -- --platform darwin --version 0.20.0 npm run binary-upload -- --platform darwin --version 0.20.0 --zip cypress.zip ``` -If something goes wrong, see debug messages using `DEBUG=cypress:binary ...` environment +If something goes wrong, see the debug messages using the `DEBUG=cypress:binary ...` environment variable. -Because we had many problems reliably zipping built binary, for now we need -to build both Mac and Linux binary from Mac (Linux binary is built using +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. ### Linux Docker -If you're Mac you can build the linux binary if you have docker installed. +If you are using a Mac you can build the linux binary if you have docker installed. ``` npm run binary-build-linux @@ -71,37 +75,38 @@ npm run binary-build-linux 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 +impossible. Instead we: + +- Build the package (with the new target version baked in) and the binary. +- Build the Linux and Windows binaries on CircleCI and AppVeyor. +- Upload the binaries *and the new NPM package* to the Cypress CDN under the "beta" url. +- Launch the test CI 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) with unique urls instead of installing from the NPM registry. -- Build the package (with new target version baked in) and the binary. - We build Linux and Windows binaries on CircleCI and AppVeyor. -- upload binaries *and the new NPM package* to Cypress CDN under "beta" url -- launch test CI 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) with unique urls instead of installing from the NPM registry. -A typical installation looks like this +A typical installation looks like this: ``` -export CYPRESS_BINARY_VERSION=https://cdn.../binary//hash/cypress.zip +export CYPRESS_INSTALL_BINARY=https://cdn.../binary//hash/cypress.zip npm i https://cdn.../npm//hash/cypress.tgz ``` - All test projects are triggered automatically by the build projects, but we need to look at CIs - to make sure new binary and NPM package really work without breaking any of the tests. -- Each binary and NPM package has new version inside and in the URL, for example `1.0.5`. The url + to make sure the new binary and NPM package really work without breaking any of the tests. +- Each binary and NPM package has the new version inside and in the URL, for example `1.0.5`. The url also contains the original commit SHA from which it was built. - Build the Mac binary and upload (see above) to the CDN. Make sure to build it from the - same commit as the binaries built by CI + same commit as the binaries built by CI. - The upload from Mac binary will create new folder on CDN like `https://cdn.../desktop/1.0.5/osx64`. We need to create parallel subfolders for - Windows and Linux binaries. Go to AWS console and create them. In this case you would create + Windows and Linux binaries. Go to the AWS console and create them. In this case you would create folders `desktop/1.0.5/linux64` and `desktop/1.0.5/win64`. -- Copy _the tested binaries_ from unique `binary` folder into `desktop/1.0.5` subfolders for each +- Copy _the tested binaries_ from the unique `binary` folder into `desktop/1.0.5` subfolders for each platform. -- Publish the new NPM package under dev tag. The unique link to the package file `cypress.tgz` - is the one already tested above. You can publish to NPM registry straight from the URL +- 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: $ npm publish https://cdn.../npm/1.0.5//cypress.tgz --tag dev + cypress@1.0.5 -- Check that new version has the right tag using +- Check that the new version has the right tag using [available-versions](https://github.com/bahmutov/available-versions) ``` @@ -112,15 +117,14 @@ $ vers cypress ``` - Test `cypress@1.0.5` again to make sure everything is working. You can trigger test projects - from command line (if you have appropriate permissions) + from command line (if you have the appropriate permissions) node scripts/test-other-projects.js --npm cypress@1.0.5 --binary 1.0.5 -- write changelog -- publish changelog -- close issues (link to changelog) -- update npm dist tag to `latest` using `npm dist-tag add cypress@1.0.5` -- update `manifest.json` for download server `npm run binary-release -- --version 1.0.5` -- push out updated changes to manifest for `on.cypress.io` if needed +- Update and publish the [changelog](https://github.com/cypress-io/cypress-documentation/blob/develop/source/guides/references/changelog.md) +- Close issues (with a link to the changelog). +- Update the NPM dist tag to `latest` using `npm dist-tag add cypress@1.0.5`. +- Update the `manifest.json` for download server `npm run binary-release -- --version 1.0.5` +- Push out the updated changes to the manifest for `on.cypress.io` if needed. -Take a break, you deserve it! +Take a break, you deserve it! :sunglasses: diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 78b52342a41a..a5ab0b7a72ea 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,28 +1,21 @@ - - -- Operating System: -- Cypress Version: -- Browser Version: - ### Is this a Feature or Bug? ### Current behavior: + ### Desired behavior: +### Steps to reproduce: -### How to reproduce: - - -#### Test code: + ```js ``` +### Versions -### Additional Info (images, stack traces, etc) - + diff --git a/circle.yml b/circle.yml index cfc001b09503..85e749ff5403 100644 --- a/circle.yml +++ b/circle.yml @@ -7,7 +7,15 @@ defaults: &defaults # the Docker image with Cypress dependencies and Chrome browser - image: cypress/browsers:chrome64 environment: - CIRCLE_ARTIFACTS: /tmp/artifacts ## store artifacts here + ## set specific timezone + TZ: "/usr/share/zoneinfo/America/New_York" + + ## store artifacts here + CIRCLE_ARTIFACTS: /tmp/artifacts + + ## set so that e2e tests are consistent + COLUMNS: 100 + LINES: 24 jobs: ## code checkout and NPM installs @@ -22,6 +30,12 @@ jobs: - run: name: print global NPM cache path command: echo $(npm -g bin) + - run: + name: print Node version + command: node -v + - run: + name: print NPM version + command: npm -v - run: npm run check-node-version ## make sure the TERM is set to 'xterm' in node @@ -30,11 +44,12 @@ jobs: ## * http://andykdocs.de/development/Docker/Fixing+the+Docker+TERM+variable+issue ## * https://unix.stackexchange.com/questions/43945/whats-the-difference-between-various-term-variables - run: - name: Checking TERM is set + name: Checking TERM and COLUMNS are set command: | - echo 'term env var is:' $TERM - node -e 'assert.equal(process.env.TERM, "xterm", "need TERM to be set for Docker to work")' - node -e 'console.log("TERM %s stdout.isTTY?", process.env.TERM, process.stdout.isTTY)' + node -e 'assert.ok(process.env.TERM === "xterm", `process.env.TERM=${process.env.TERM} and must be set to "xterm" for Docker to work`)' + node -e 'assert.ok(process.env.COLUMNS === "100", `process.env.COLUMNS=${process.env.COLUMNS} must be set to 100 for snapshots to pass`)' + node -e 'console.log("stdout.isTTY?", process.stdout.isTTY)' + node -e 'console.log("stderr.isTTY?", process.stderr.isTTY)' # need to restore a separate cache for each package.json - restore_cache: @@ -599,7 +614,7 @@ jobs: name: Install Cypress working_directory: test-binary # force installing the freshly built binary - command: CYPRESS_BINARY_VERSION=/tmp/urls/cypress.zip npm i ../cli/build + command: CYPRESS_INSTALL_BINARY=/tmp/urls/cypress.zip npm i ../cli/build - run: name: Verify Cypress binary working_directory: test-binary @@ -624,7 +639,7 @@ jobs: name: Install Cypress working_directory: /tmp/cypress-test-tiny # force installing the freshly built binary - command: CYPRESS_BINARY_VERSION=/tmp/urls/cypress.zip npm i /tmp/urls/cypress.tgz + command: CYPRESS_INSTALL_BINARY=/tmp/urls/cypress.zip npm i /tmp/urls/cypress.tgz - run: name: Run test project working_directory: /tmp/cypress-test-tiny @@ -712,7 +727,6 @@ workflows: branches: only: - develop - - fix-engines-1373 requires: - build - build-binary: @@ -720,7 +734,6 @@ workflows: branches: only: - develop - - fix-engines-1373 requires: - build - test-next-version: @@ -728,7 +741,6 @@ workflows: branches: only: - develop - - fix-engines-1373 requires: - build-npm-package - build-binary @@ -745,7 +757,6 @@ workflows: branches: only: - develop - - issue-895 requires: - build-npm-package - build-binary diff --git a/cli/__snapshots__/build_spec.js b/cli/__snapshots__/build_spec.js index ea46540d4d0b..794e4d72ee55 100644 --- a/cli/__snapshots__/build_spec.js +++ b/cli/__snapshots__/build_spec.js @@ -13,6 +13,21 @@ exports['package.json build outputs expected properties 1'] = { "type": "git", "url": "https://github.com/cypress-io/cypress.git" }, + "keywords": [ + "browser", + "cypress", + "cypress.io", + "automation", + "end-to-end", + "e2e", + "integration", + "mocks", + "test", + "testing", + "runner", + "spies", + "stubs" + ], "types": "types", "scripts": { "postinstall": "node index.js --exec install", diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index b0bf0dc880e4..7e9c588b7d61 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -1,3 +1,82 @@ +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 runs Cypress on a specific port. overrides any value in cypress.json. + -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json + -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. + -d, --detached [bool] runs Cypress application in detached mode + -P, --project path to the project + --global force Cypress into global mode as if its globally installed + --dev runs cypress in development and bypasses binary check + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` + +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 your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. + -s, --spec runs a specific spec file. defaults to "all" + -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" + -o, --reporter-options options for the mocha reporter. defaults to "null" + -p, --port runs Cypress on a specific port. overrides any value in cypress.json. + -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json + -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. + -b, --browser runs Cypress in the browser with the given name. note: using an external browser will not record a video. + -P, --project path to the project + --dev runs cypress in development and bypasses binary check + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` + exports['cli help command shows help 1'] = ` command: bin/cypress help @@ -20,12 +99,12 @@ exports['cli help command shows help 1'] = ` 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 Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + 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 Verifies that Cypress is installed correctly and executable ------- stderr: ------- @@ -56,12 +135,12 @@ exports['cli help command shows help for -h 1'] = ` 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 Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + 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 Verifies that Cypress is installed correctly and executable ------- stderr: ------- @@ -92,12 +171,12 @@ exports['cli help command shows help for --help 1'] = ` 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 Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + 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 Verifies that Cypress is installed correctly and executable ------- stderr: ------- @@ -130,12 +209,12 @@ exports['cli unknown command shows usage and exits 1'] = ` 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 Installs the Cypress executable matching this package's version - verify Verifies that Cypress is installed correctly and executable + 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 Verifies that Cypress is installed correctly and executable ------- stderr: ------- @@ -145,26 +224,31 @@ exports['cli unknown command shows usage and exits 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 ` diff --git a/cli/__snapshots__/cypress_spec.js b/cli/__snapshots__/cypress_spec.js index 88974b4c50ae..29f344f553bb 100644 --- a/cli/__snapshots__/cypress_spec.js +++ b/cli/__snapshots__/cypress_spec.js @@ -14,3 +14,7 @@ exports['cypress .run resolves with contents of tmp file 1'] = { "code": 0, "failingTests": [] } + +exports['cypress .open normalizes config object 1'] = { + "config": "pageLoadTime=10000,watchForFileChanges=false" +} diff --git a/cli/__snapshots__/download_spec.js b/cli/__snapshots__/download_spec.js index ab716acb076f..2c84fbcbca19 100644 --- a/cli/__snapshots__/download_spec.js +++ b/cli/__snapshots__/download_spec.js @@ -1,11 +1,3 @@ -exports['latest desktop url 1'] = ` -https://download.cypress.io/desktop?platform=OS&arch=ARCH -` - -exports['specific version desktop url 1'] = ` -https://download.cypress.io/desktop/0.20.2?platform=OS&arch=ARCH -` - exports['download status errors 1'] = ` Error: The Cypress App could not be downloaded. @@ -16,7 +8,15 @@ URL: https://download.cypress.io/desktop?platform=OS&arch=ARCH 404 - Not Found ---------- -Platform: darwin (test release) +Platform: darwin (Foo-OsVersion) Cypress Version: 1.2.3 ` + +exports['latest desktop url 1'] = ` +https://download.cypress.io/desktop?platform=OS&arch=ARCH +` + +exports['specific version desktop url 1'] = ` +https://download.cypress.io/desktop/0.20.2?platform=OS&arch=ARCH +` diff --git a/cli/__snapshots__/errors_spec.js b/cli/__snapshots__/errors_spec.js index 1d5ae4d7a0b4..6fc421d87d01 100644 --- a/cli/__snapshots__/errors_spec.js +++ b/cli/__snapshots__/errors_spec.js @@ -6,7 +6,10 @@ exports['errors individual has the following errors 1'] = [ "versionMismatch", "unexpected", "failedDownload", - "failedUnzip" + "failedUnzip", + "invalidCacheDirectory", + "removed", + "CYPRESS_RUN_BINARY" ] exports['errors .errors.formErrorText returns fully formed text message 1'] = ` @@ -21,6 +24,6 @@ https://on.cypress.io/required-dependencies If you are using Docker, we provide containers with all required dependencies installed. ---------- -Platform: test platform (test release) +Platform: test platform (Foo-OsVersion) Cypress Version: 1.2.3 ` diff --git a/cli/__snapshots__/install_spec.js b/cli/__snapshots__/install_spec.js index 31370b247efc..5e6646869f29 100644 --- a/cli/__snapshots__/install_spec.js +++ b/cli/__snapshots__/install_spec.js @@ -1,101 +1,117 @@ -exports['installs without existing installation 1'] = ` -Installing Cypress (version: 1.2.3) +exports['version already installed 1'] = ` +Cypress 1.2.3 is already installed in /cache/Cypress/1.2.3 - ✔ Downloaded Cypress - ✔ Unzipped Cypress - ✔ Finished Installation /path/to/binary/dir/ +Skipping installation: -You can now open Cypress by running: node_modules/.bin/cypress open + Pass the --force option if you'd like to reinstall anyway. + + +` + +exports['skip installation 1'] = ` +Note: Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0. -https://on.cypress.io/installing-cypress ` exports['specify version in env vars 1'] = ` -Forcing a binary version different than the default. +⚠ Warning: Forcing a binary version different than the default. -The CLI expected to install version: 1.2.3 + The CLI expected to install version: 1.2.3 -Instead we will install version: 0.12.1 + Instead we will install version: 0.12.1 -Note: there is no guarantee these versions will work properly together. + These versions may not work properly together. Installing Cypress (version: 0.12.1) ✔ Downloaded Cypress ✔ Unzipped Cypress - ✔ Finished Installation /path/to/binary/dir/ + ✔ Finished Installation /cache/Cypress/1.2.3 You can now open Cypress by running: node_modules/.bin/cypress open https://on.cypress.io/installing-cypress + ` -exports['version already installed 1'] = ` -Cypress 1.2.3 is already installed. Skipping installation. +exports['continues installing on failure 1'] = ` +Installing Cypress (version: 1.2.3) + + ✔ Downloaded Cypress + ✔ Unzipped Cypress + ✔ Finished Installation /cache/Cypress/1.2.3 + +You can now open Cypress by running: node_modules/.bin/cypress open + +https://on.cypress.io/installing-cypress -Pass the --force option if you'd like to reinstall anyway. ` -exports['continues installing on failure 1'] = ` +exports['installs without existing installation 1'] = ` Installing Cypress (version: 1.2.3) ✔ Downloaded Cypress ✔ Unzipped Cypress - ✔ Finished Installation /path/to/binary/dir/ + ✔ Finished Installation /cache/Cypress/1.2.3 You can now open Cypress by running: node_modules/.bin/cypress open https://on.cypress.io/installing-cypress + ` exports['installed version does not match needed version 1'] = ` -Installed version (x.x.x) does not match needed version (1.2.3). +Cypress x.x.x is already installed in /cache/Cypress/1.2.3 Installing Cypress (version: 1.2.3) ✔ Downloaded Cypress ✔ Unzipped Cypress - ✔ Finished Installation /path/to/binary/dir/ + ✔ Finished Installation /cache/Cypress/1.2.3 You can now open Cypress by running: node_modules/.bin/cypress open https://on.cypress.io/installing-cypress + ` exports['forcing true always installs 1'] = ` +Cypress 1.2.3 is already installed in /cache/Cypress/1.2.3 + Installing Cypress (version: 1.2.3) ✔ Downloaded Cypress ✔ Unzipped Cypress - ✔ Finished Installation /path/to/binary/dir/ + ✔ Finished Installation /cache/Cypress/1.2.3 You can now open Cypress by running: node_modules/.bin/cypress open https://on.cypress.io/installing-cypress + ` exports['warning installing as global 1'] = ` -Installed version (x.x.x) does not match needed version (1.2.3). +Cypress x.x.x is already installed in /cache/Cypress/1.2.3 Installing Cypress (version: 1.2.3) ✔ Downloaded Cypress ✔ Unzipped Cypress - ✔ Finished Installation /path/to/binary/dir/ + ✔ Finished Installation /cache/Cypress/1.2.3 -It looks like you've installed Cypress globally. +⚠ Warning: It looks like you've installed Cypress globally. -This will work, but it's not recommended. + This will work, but it's not recommended. -The recommended way to install Cypress is as a devDependency per project. + The recommended way to install Cypress is as a devDependency per project. -You should probably run these commands: + You should probably run these commands: - npm uninstall -g cypress - npm install --save-dev cypress @@ -103,7 +119,7 @@ You should probably run these commands: ` exports['installing in ci 1'] = ` -Installed version (x.x.x) does not match needed version (1.2.3). +Cypress x.x.x is already installed in /cache/Cypress/1.2.3 Installing Cypress (version: 1.2.3) @@ -118,8 +134,30 @@ You can now open Cypress by running: node_modules/.bin/cypress open https://on.cypress.io/installing-cypress + ` -exports['skip installation 1'] = ` -Skipping binary installation. Env var 'CYPRESS_SKIP_BINARY_INSTALL' was found. + +exports['invalid cache directory 1'] = ` +Error: Cypress cannot write to the cache directory due to file permissions +---------- + +Failed to access /invalid/cache/dir: + +EACCES: permission denied, mkdir '/invalid' +---------- + +Platform: darwin (Foo-OsVersion) +Cypress Version: 1.2.3 + +` + +exports['error for removed CYPRESS_BINARY_VERSION 1'] = ` +Error: The environment variable CYPRESS_BINARY_VERSION has been renamed to CYPRESS_INSTALL_BINARY as of version 3.0.0 + +You should setCYPRESS_INSTALL_BINARY instead. +---------- + +Platform: darwin (Foo-OsVersion) +Cypress Version: 1.2.3 ` diff --git a/cli/__snapshots__/unzip_spec.js b/cli/__snapshots__/unzip_spec.js index 682c3b910e98..afe2aa0d8755 100644 --- a/cli/__snapshots__/unzip_spec.js +++ b/cli/__snapshots__/unzip_spec.js @@ -9,7 +9,7 @@ https://github.com/cypress-io/cypress/issues Error: end of central directory record signature not found ---------- -Platform: darwin (test release) +Platform: darwin (Foo-OsVersion) Cypress Version: 1.2.3 ` diff --git a/cli/__snapshots__/verify_spec.js b/cli/__snapshots__/verify_spec.js index a2d68bae7b7d..e9f5427e2d7e 100644 --- a/cli/__snapshots__/verify_spec.js +++ b/cli/__snapshots__/verify_spec.js @@ -1,35 +1,46 @@ +exports['verbose stdout output 1'] = ` +It looks like this is your first time using Cypress: 1.2.3 + + ✔ Verified Cypress! /cache/Cypress/1.2.3/Cypress.app + +Opening Cypress... + +` + exports['no version of Cypress installed 1'] = ` -Error: No version of Cypress is installed. +Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app Please reinstall Cypress by running: cypress install ---------- -Cypress executable not found at: /path/to/executable +Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/executable ---------- -Platform: darwin (test release) +Platform: darwin (Foo-OsVersion) Cypress Version: 1.2.3 ` exports['warning installed version does not match verified version 1'] = ` -Installed version bloop does not match the expected package version 1.2.3 +Found binary version bloop installed in: /cache/Cypress/1.2.3/Cypress.app + +⚠ Warning: Binary version bloop does not match the expected package version 1.2.3 -Note: there is no guarantee these versions will work properly together. + These versions may not work properly together. ` exports['executable cannot be found 1'] = ` -Error: No version of Cypress is installed. +Error: No version of Cypress is installed in: /cache/Cypress/1.2.3/Cypress.app Please reinstall Cypress by running: cypress install ---------- -Cypress executable not found at: /path/to/executable +Cypress executable not found at: /cache/Cypress/1.2.3/Cypress.app/executable ---------- -Platform: darwin (test release) +Platform: darwin (Foo-OsVersion) Cypress Version: 1.2.3 ` @@ -37,7 +48,7 @@ Cypress Version: 1.2.3 exports['verification with executable 1'] = ` It looks like this is your first time using Cypress: 1.2.3 - ✔ Verified Cypress! /path/to/executable/dir + ✔ Verified Cypress! /cache/Cypress/1.2.3/Cypress.app Opening Cypress... @@ -46,7 +57,7 @@ Opening Cypress... exports['fails verifying Cypress 1'] = ` It looks like this is your first time using Cypress: 1.2.3 - ✖ Verifying Cypress can run /path/to/executable/dir + ✖ Verifying Cypress can run /cache/Cypress/1.2.3/Cypress.app STRIPPED Error: Cypress failed to start. @@ -62,7 +73,7 @@ If you are using Docker, we provide containers with all required dependencies in an error about dependencies ---------- -Platform: darwin (test release) +Platform: darwin (Foo-OsVersion) Cypress Version: 1.2.3 ` @@ -70,45 +81,56 @@ Cypress Version: 1.2.3 exports['no existing version verified 1'] = ` It looks like this is your first time using Cypress: 1.2.3 - ✔ Verified Cypress! /path/to/executable/dir + ✔ Verified Cypress! /cache/Cypress/1.2.3/Cypress.app Opening Cypress... ` exports['current version has not been verified 1'] = ` -It looks like this is your first time using Cypress: 1.2.3 +Found binary version different version installed in: /cache/Cypress/1.2.3/Cypress.app + +⚠ Warning: Binary version different version does not match the expected package version 1.2.3 + + These versions may not work properly together. - ✔ Verified Cypress! /path/to/executable/dir +It looks like this is your first time using Cypress: different version + + ✔ Verified Cypress! /cache/Cypress/1.2.3/Cypress.app Opening Cypress... ` exports['current version has not been verified 2'] = ` -Installed version 9.8.7 does not match the expected package version 1.2.3 +Found binary version 9.8.7 installed in: /cache/Cypress/1.2.3/Cypress.app + +⚠ Warning: Binary version 9.8.7 does not match the expected package version 1.2.3 -Note: there is no guarantee these versions will work properly together. + These versions may not work properly together. It looks like this is your first time using Cypress: 9.8.7 - ✔ Verified Cypress! /path/to/executable/dir + ✔ Verified Cypress! /cache/Cypress/1.2.3/Cypress.app Opening Cypress... ` exports['no welcome message 1'] = ` -It looks like this is your first time using Cypress: 1.2.3 +Found binary version different version installed in: /cache/Cypress/1.2.3/Cypress.app + +⚠ Warning: Binary version different version does not match the expected package version 1.2.3 + + These versions may not work properly together. - ✔ Verified Cypress! /path/to/executable/dir ` exports['xvfb fails 1'] = ` It looks like this is your first time using Cypress: 1.2.3 - ✖ Verifying Cypress can run /path/to/executable/dir + ✖ Verifying Cypress can run /cache/Cypress/1.2.3/Cypress.app STRIPPED Error: Your system is missing the dependency: XVFB @@ -124,7 +146,7 @@ If you are using Docker, we provide containers with all required dependencies in Caught error trying to run XVFB: "test without xvfb" ---------- -Platform: darwin (test release) +Platform: darwin (Foo-OsVersion) Cypress Version: 1.2.3 ` @@ -132,18 +154,67 @@ Cypress Version: 1.2.3 exports['verifying in ci 1'] = ` It looks like this is your first time using Cypress: 1.2.3 -[xx:xx:xx] Verifying Cypress can run /path/to/executable/dir [started] -[xx:xx:xx] Verifying Cypress can run /path/to/executable/dir [completed] +[xx:xx:xx] Verifying Cypress can run /cache/Cypress/1.2.3/Cypress.app [started] +[xx:xx:xx] Verifying Cypress can run /cache/Cypress/1.2.3/Cypress.app [completed] Opening Cypress... ` -exports['verbose stdout output 1'] = ` +exports['valid CYPRESS_RUN_BINARY 1'] = ` +Note: You have set the environment variable: CYPRESS_RUN_BINARY=/custom/Contents/MacOS/Cypress: + + This overrides the default Cypress binary path used. + It looks like this is your first time using Cypress: 1.2.3 - ✔ Verified Cypress! /path/to/executable/dir + ✔ Verified Cypress! /real/custom Opening Cypress... ` + +exports['darwin: error when invalid CYPRESS_RUN_BINARY 1'] = ` +Note: You have set the environment variable: CYPRESS_RUN_BINARY=/custom/: + + This overrides the default Cypress binary path used. + +Error: Could not run binary set by environment variable CYPRESS_RUN_BINARY=/custom/ + +Ensure the environment variable is a path to the Cypress binary, matching **/Contents/MacOS/Cypress +---------- + +Platform: darwin (Foo-OsVersion) +Cypress Version: 1.2.3 + +` + +exports['linux: error when invalid CYPRESS_RUN_BINARY 1'] = ` +Note: You have set the environment variable: CYPRESS_RUN_BINARY=/custom/: + + This overrides the default Cypress binary path used. + +Error: Could not run binary set by environment variable CYPRESS_RUN_BINARY=/custom/ + +Ensure the environment variable is a path to the Cypress binary, matching **/Cypress +---------- + +Platform: linux (Foo-OsVersion) +Cypress Version: 1.2.3 + +` + +exports['win32: error when invalid CYPRESS_RUN_BINARY 1'] = ` +Note: You have set the environment variable: CYPRESS_RUN_BINARY=/custom/: + + This overrides the default Cypress binary path used. + +Error: Could not run binary set by environment variable CYPRESS_RUN_BINARY=/custom/ + +Ensure the environment variable is a path to the Cypress binary, matching **/Cypress.exe +---------- + +Platform: win32 (Foo-OsVersion) +Cypress Version: 1.2.3 + +` diff --git a/cli/lib/cli.js b/cli/lib/cli.js index 4d22b703ce61..bb1c274667fa 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -5,6 +5,18 @@ const debug = require('debug')('cypress:cli') const util = require('./util') const logger = require('./logger') +// patch "commander" method called when a user passed an unknown option +// we want to print help for the current command and exit with an error +commander.Command.prototype.unknownOption = function (flag) { + if (this._allowUnknownOption) return + logger.error() + logger.error(' error: unknown option:', flag) + logger.error() + this.outputHelp() + logger.error() + process.exit(1) +} + const coerceFalse = (arg) => { return arg !== 'false' } @@ -13,8 +25,7 @@ const parseOpts = (opts) => { opts = _.pick(opts, 'project', 'spec', 'reporter', 'reporterOptions', 'path', 'destination', 'port', 'env', 'cypressVersion', 'config', 'record', 'key', - 'browser', 'detached', 'headed', - 'group', 'groupId', 'global', 'dev') + 'browser', 'detached', 'headed', 'global', 'dev', 'force') debug('parsed cli options', opts) @@ -39,9 +50,8 @@ const descriptions = { global: 'force Cypress into global mode as if its globally installed', version: 'Prints Cypress version', headed: 'displays the Electron browser instead of running headlessly', - group: 'flag to group individual runs by using common --group-id', - groupId: 'optional common id to group runs by, extracted from CI environment variables by default', dev: 'runs cypress in development and bypasses binary check', + forceInstall: 'force install the Cypress binary', } const knownCommands = ['version', 'run', 'open', 'install', 'verify', '-v', '--version', 'help', '-h', '--help'] @@ -111,8 +121,6 @@ module.exports = { .option('-c, --config ', text('config')) .option('-b, --browser ', text('browser')) .option('-P, --project ', text('project')) - .option('--group', text('group'), coerceFalse) - .option('--group-id ', text('groupId')) .option('--dev', text('dev'), coerceFalse) .action((opts) => { debug('running Cypress') @@ -142,23 +150,32 @@ module.exports = { program .command('install') + .usage('[options]') .description('Installs the Cypress executable matching this package\'s version') - .action(() => { + .option('-f, --force', text('forceInstall')) + .action((opts) => { require('./tasks/install') - .start({ force: true }) + .start(parseOpts(opts)) .catch(util.logErrorExit1) }) program .command('verify') + .usage('[options]') .description('Verifies that Cypress is installed correctly and executable') - .action(() => { + .action((opts) => { + const defaultOpts = { force: true, welcomeMessage: false } + const parsedOpts = parseOpts(opts) + const options = _.extend(parsedOpts, defaultOpts) require('./tasks/verify') - .start({ force: true, welcomeMessage: false }) + .start(options) .catch(util.logErrorExit1) }) + logger.log() + debug('cli starts with arguments %j', args) + util.printNodeOptions() // if there are no arguments if (args.length <= 2) { @@ -167,6 +184,8 @@ module.exports = { // exits } + // Deprecated Catches + const firstCommand = args[2] if (!_.includes(knownCommands, firstCommand)) { debug('unknwon command %s', firstCommand) diff --git a/cli/lib/cypress.js b/cli/lib/cypress.js index ef19c4568133..9fca6b4a5472 100644 --- a/cli/lib/cypress.js +++ b/cli/lib/cypress.js @@ -10,6 +10,7 @@ const util = require('./util') const cypressModuleApi = { open (options = {}) { + options = util.normalizeModuleOptions(options) return open.start(options) }, diff --git a/cli/lib/errors.js b/cli/lib/errors.js index ca6b8d92f2fb..9a84b2b43c43 100644 --- a/cli/lib/errors.js +++ b/cli/lib/errors.js @@ -1,11 +1,10 @@ const os = require('os') const chalk = require('chalk') -const Promise = require('bluebird') -const getos = Promise.promisify(require('getos')) const { stripIndent, stripIndents } = require('common-tags') const { merge } = require('ramda') const util = require('./util') +const state = require('./tasks/state') const issuesUrl = 'https://github.com/cypress-io/cypress/issues' const docsUrl = 'https://on.cypress.io' @@ -26,12 +25,12 @@ const failedUnzip = { `, } -const missingApp = { - description: 'No version of Cypress is installed.', +const missingApp = (binaryDir) => ({ + description: `No version of Cypress is installed in: ${chalk.cyan(binaryDir)}`, solution: stripIndent` \nPlease reinstall Cypress by running: ${chalk.cyan('cypress install')} `, -} +}) const nonZeroExitCodeXvfb = { description: 'XVFB exited with a non zero exit code.', @@ -69,6 +68,11 @@ const missingDependency = { `, } +const invalidCacheDirectory = { + description: 'Cypress cannot write to the cache directory due to file permissions', + solution: '', +} + const versionMismatch = { description: 'Installed version does not match package version.', solution: 'Install Cypress and verify app again', @@ -89,18 +93,37 @@ const unexpected = { `, } -const getOsVersion = () => { - if (os.platform() === 'linux') { - return getos() - .then((osInfo) => [osInfo.dist, osInfo.release].join(' - ')) - .catch(() => os.release()) - } else { - return Promise.resolve(os.release()) - } +const removed = { + CYPRESS_BINARY_VERSION: { + description: stripIndent` + The environment variable CYPRESS_BINARY_VERSION has been renamed to CYPRESS_INSTALL_BINARY as of version ${chalk.green('3.0.0')} + `, + solution: stripIndent` + You should setCYPRESS_INSTALL_BINARY instead. + `, + }, + CYPRESS_SKIP_BINARY_INSTALL: { + description: stripIndent` + The environment variable CYPRESS_SKIP_BINARY_INSTALL has been removed as of version ${chalk.green('3.0.0')} + `, + solution: stripIndent` + To skip the binary install, set CYPRESS_INSTALL_BINARY=0 + `, + }, +} + +const CYPRESS_RUN_BINARY = { + notValid: (value) => { + const properFormat = `**/${state.getPlatformExecutable()}` + return { + description: `Could not run binary set by environment variable CYPRESS_RUN_BINARY=${value}`, + solution: `Ensure the environment variable is a path to the Cypress binary, matching ${properFormat}`, + } + }, } function getPlatformInfo () { - return getOsVersion() + return util.getOsVersionAsync() .then((version) => stripIndent` Platform: ${os.platform()} (${version}) Cypress Version: ${util.pkgVersion()} @@ -185,5 +208,8 @@ module.exports = { unexpected, failedDownload, failedUnzip, + invalidCacheDirectory, + removed, + CYPRESS_RUN_BINARY, }, } diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index 22265148c41d..d0167a47bc95 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -10,7 +10,7 @@ const processRunOptions = (options = {}) => { const args = ['--run-project', options.project] - //// if key is set use that - else attempt to find it by env var + //// 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 = process.env.CYPRESS_RECORD_KEY || process.env.CYPRESS_CI_KEY diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index 0d8cba950e44..b0f8b0d98694 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -1,25 +1,39 @@ const _ = require('lodash') const os = require('os') const cp = require('child_process') -const tty = require('tty') const path = require('path') const Promise = require('bluebird') const debug = require('debug')('cypress:cli') const util = require('../util') -const info = require('../tasks/info') +const state = require('../tasks/state') const xvfb = require('./xvfb') const { throwFormErrorText, errors } = require('../errors') -const isXlibOrLibudevRe = /^(Xlib|libudev)/ +const isXlibOrLibudevRe = /^(?:Xlib|libudev)/ +const isHighSierraWarningRe = /\*\*\* WARNING/ -function needsStderrPipe (needsXvfb) { - return needsXvfb && os.platform() === 'linux' +function isPlatform (platform) { + return os.platform() === platform +} + +function needsStderrPiped (needsXvfb) { + return isPlatform('darwin') || (needsXvfb && isPlatform('linux')) +} + +function needsEverythingPipedDirectly () { + return isPlatform('win32') } function getStdio (needsXvfb) { + if (needsEverythingPipedDirectly()) { + return 'pipe' + } + // https://github.com/cypress-io/cypress/issues/921 - if (needsStderrPipe(needsXvfb)) { + // https://github.com/cypress-io/cypress/issues/1143 + // https://github.com/cypress-io/cypress/issues/1745 + if (needsStderrPiped(needsXvfb)) { // returning pipe here so we can massage stderr // and remove garbage from Xlib and libuv // due to starting the XVFB process on linux @@ -32,6 +46,11 @@ function getStdio (needsXvfb) { module.exports = { start (args, options = {}) { const needsXvfb = xvfb.isNeeded() + let executable = state.getPathToExecutable(state.getBinaryDir()) + + if (process.env.CYPRESS_RUN_BINARY) { + executable = process.env.CYPRESS_RUN_BINARY + } debug('needs XVFB?', needsXvfb) @@ -39,63 +58,49 @@ module.exports = { args = [].concat(args, '--cwd', process.cwd()) _.defaults(options, { + env: process.env, detached: false, stdio: getStdio(needsXvfb), }) const spawn = () => { return new Promise((resolve, reject) => { - let cypressPath = info.getPathToExecutable() - if (options.dev) { // if we're in dev then reset // the launch cmd to be 'npm run dev' - cypressPath = 'node' + executable = 'node' args.unshift(path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js')) } - debug('spawning Cypress %s', cypressPath) - debug('spawn args %j', args, options) + const overrides = util.getEnvOverrides() + + debug('spawning Cypress with executable: %s', executable) + debug('spawn forcing env overrides %o', overrides) + debug('spawn args %o %o', args, _.omit(options, 'env')) // strip dev out of child process options options = _.omit(options, 'dev') + options = _.omit(options, 'binaryFolder') - // when running in electron in windows - // it never supports color but we're - // going to force it anyway as long - // as our parent cli process can support - // colors! - // - // also when we are in linux and using the 'pipe' - // option our process.stderr.isTTY will not be true - // which ends up disabling the colors =( - if (util.supportsColor()) { - process.env.FORCE_COLOR = 1 - process.env.DEBUG_COLORS = 1 - process.env.MOCHA_COLORS = 1 - } - - // if we needed to pipe stderr and we're currently - // a tty on stderr - if (needsStderrPipe(needsXvfb) && tty.isatty(2)) { - // then force stderr tty - // - // this is necessary because we want our child - // electron browser to behave _THE SAME WAY_ as - // if we aren't using pipe. pipe is necessary only - // to filter out garbage on stderr -____________- - process.env.FORCE_STDERR_TTY = 1 - } + // figure out if we're going to be force enabling or disabling colors. + // also figure out whether we should force stdout and stderr into thinking + // it is a tty as opposed to a pipe. + options.env = _.extend({}, options.env, overrides) - const child = cp.spawn(cypressPath, args, options) + const child = cp.spawn(executable, args, options) child.on('close', resolve) child.on('error', reject) + child.stdin && child.stdin.pipe(process.stdin) + child.stdout && child.stdout.pipe(process.stdout) + // if this is defined then we are manually piping for linux // to filter out the garbage child.stderr && child.stderr.on('data', (data) => { - // bail if this is a line from xlib or libudev - if (isXlibOrLibudevRe.test(data.toString())) { + const str = data.toString() + + // bail if this is warning line garbage + if (isXlibOrLibudevRe.test(str) || isHighSierraWarningRe.test(str)) { return } @@ -109,8 +114,10 @@ module.exports = { }) } - const userFriendlySpawn = () => - spawn().catch(throwFormErrorText(errors.unexpected)) + const userFriendlySpawn = () => { + return spawn() + .catch(throwFormErrorText(errors.unexpected)) + } if (needsXvfb) { return xvfb.start() diff --git a/cli/lib/exec/versions.js b/cli/lib/exec/versions.js index 8e5493896934..1ee0374950b9 100644 --- a/cli/lib/exec/versions.js +++ b/cli/lib/exec/versions.js @@ -1,12 +1,12 @@ const util = require('../util') -const info = require('../tasks/info') +const state = require('../tasks/state') const getVersions = () => { - return info.getInstalledVersion() - .then((binary) => { + return state.getBinaryPkgVersionAsync() + .then((binaryVersion) => { return { package: util.pkgVersion(), - binary: binary || 'not installed', + binary: binaryVersion || 'not installed', } }) } diff --git a/cli/lib/exec/xvfb.js b/cli/lib/exec/xvfb.js index 3223c8aff665..148525b2f09a 100644 --- a/cli/lib/exec/xvfb.js +++ b/cli/lib/exec/xvfb.js @@ -7,6 +7,7 @@ const debugXvfb = require('debug')('cypress:xvfb') const { throwFormErrorText, errors } = require('../errors') const xvfb = Promise.promisifyAll(new Xvfb({ + timeout: 5000, // milliseconds onStderrData (data) { if (debugXvfb.enabled) { debugXvfb(data.toString()) diff --git a/cli/lib/tasks/download.js b/cli/lib/tasks/download.js index efd3f30bbf8a..ab02a2365784 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -1,18 +1,17 @@ -const _ = require('lodash') +const la = require('lazy-ass') +const is = require('check-more-types') const os = require('os') -const path = require('path') -const progress = require('request-progress') -const Promise = require('bluebird') -const request = require('request') const url = require('url') +const path = require('path') const debug = require('debug')('cypress:cli') +const request = require('request') +const Promise = require('bluebird') +const requestProgress = require('request-progress') const { stripIndent } = require('common-tags') -const is = require('check-more-types') const { throwFormErrorText, errors } = require('../errors') const fs = require('../fs') const util = require('../util') -const info = require('./info') const baseUrl = 'https://download.cypress.io/' @@ -46,36 +45,26 @@ const prettyDownloadErr = (err, version) => { return throwFormErrorText(errors.failedDownload)(msg) } -// attention: -// when passing relative path to NPM post install hook, the current working -// directory is set to the `node_modules/cypress` folder -// the user is probably passing relative path with respect to root package folder -function formAbsolutePath (filename) { - if (path.isAbsolute(filename)) { - return filename - } - return path.join(process.cwd(), '..', '..', filename) -} - // downloads from given url // return an object with // {filename: ..., downloaded: true} -const downloadFromUrl = (options) => { +const downloadFromUrl = ({ url, downloadDestination, progress }) => { return new Promise((resolve, reject) => { - const url = getUrl(options.version) - debug('Downloading from', url) - debug('Saving file to', options.downloadDestination) + debug('Saving file to', downloadDestination) + + let redirectVersion const req = request({ url, followRedirect (response) { const version = response.headers['x-version'] + debug('redirect version:', version) if (version) { // set the version in options if we have one. // this insulates us from potential redirect // problems where version would be set to undefined. - options.version = version + redirectVersion = version } // yes redirect @@ -86,8 +75,8 @@ const downloadFromUrl = (options) => { // closure let started = null - progress(req, { - throttle: options.throttle, + requestProgress(req, { + throttle: progress.throttle, }) .on('response', (response) => { // start counting now once we've gotten @@ -118,75 +107,39 @@ const downloadFromUrl = (options) => { const eta = util.calculateEta(state.percent, elapsed) // send up our percent and seconds remaining - options.onProgress(state.percent, util.secsRemaining(eta)) + progress.onProgress(state.percent, util.secsRemaining(eta)) }) // save this download here - .pipe(fs.createWriteStream(options.downloadDestination)) + .pipe(fs.createWriteStream(downloadDestination)) .on('finish', () => { debug('downloading finished') - resolve({ - filename: options.downloadDestination, - downloaded: true, - }) + resolve(redirectVersion) }) }) } -// returns an object with zip filename -// and a flag if the file was really downloaded -// or not. Maybe it was already there! -// {filename: ..., downloaded: true|false} -const download = (options = {}) => { - if (!options.version) { - debug('empty Cypress version to download, will try latest') - return downloadFromUrl(options) +const start = ({ version, downloadDestination, progress }) => { + if (!downloadDestination) { + la(is.unemptyString(downloadDestination), 'missing download dir', arguments) + } + if (!progress) { + progress = { onProgress: () => ({}) } } - debug('need to download Cypress version %s', options.version) - // first check the original filename - return fs.pathExists(options.version).then((exists) => { - if (exists) { - debug('found file right away', options.version) - return { - filename: options.version, - downloaded: false, - } - } - - const possibleFile = formAbsolutePath(options.version) - debug('checking local file', possibleFile, 'cwd', process.cwd()) - return fs.pathExists(possibleFile).then((exists) => { - if (exists) { - debug('found local file', possibleFile) - debug('skipping download') - return { - filename: possibleFile, - downloaded: false, - } - } else { - return downloadFromUrl(options) - } - }) - }) -} + const url = getUrl(version) + progress.throttle = 100 -const start = (options) => { - _.defaults(options, { - version: null, - throttle: 100, - onProgress: () => {}, - downloadDestination: path.join(info.getInstallationDir(), 'cypress.zip'), - }) + debug('needed Cypress version: %s', version) + debug(`downloading cypress.zip to "${downloadDestination}"`) - // make sure our 'dist' installation dir exists - return info - .ensureInstallationDir() + // ensure download dir exists + return fs.ensureDirAsync(path.dirname(downloadDestination)) .then(() => { - return download(options) + return downloadFromUrl({ url, downloadDestination, progress }) }) .catch((err) => { - return prettyDownloadErr(err, options.version) + return prettyDownloadErr(err, version) }) } diff --git a/cli/lib/tasks/info.js b/cli/lib/tasks/info.js deleted file mode 100644 index 0f9eb83c0335..000000000000 --- a/cli/lib/tasks/info.js +++ /dev/null @@ -1,94 +0,0 @@ -const _ = require('lodash') -const os = require('os') -const path = require('path') -const debug = require('debug')('cypress:cli') - -const fs = require('../fs') - -const getPlatformExecutable = () => { - const platform = os.platform() - switch (platform) { - case 'darwin': return 'Cypress.app/Contents/MacOS/Cypress' - case 'linux': return 'Cypress/Cypress' - case 'win32': return 'Cypress/Cypress.exe' - // TODO handle this error using our standard - default: throw new Error(`Platform: "${platform}" is not supported.`) - } -} - -const getInstallationDir = () => { - return path.join(__dirname, '..', '..', 'dist') -} - -const getInfoFilePath = () => { - const infoPath = path.join(getInstallationDir(), 'info.json') - debug('path to info.json file %s', infoPath) - return infoPath -} - -const getInstalledVersion = () => { - return ensureFileInfoContents() - .tap(debug) - .get('version') -} - -const getVerifiedVersion = () => { - return ensureFileInfoContents().get('verifiedVersion') -} - -const ensureInstallationDir = () => { - return fs.ensureDirAsync(getInstallationDir()) -} - -const clearVersionState = () => { - return ensureFileInfoContents() - .then((contents) => { - return writeInfoFileContents(_.omit(contents, 'version', 'verifiedVersion')) - }) -} - -const writeInstalledVersion = (version) => { - return ensureFileInfoContents() - .then((contents) => { - return writeInfoFileContents(_.extend(contents, { version })) - }) -} - -const getPathToExecutable = () => { - return path.join(getInstallationDir(), getPlatformExecutable()) -} - -const getPathToUserExecutableDir = () => { - return path.join(getInstallationDir(), getPlatformExecutable().split('/')[0]) -} - -const getInfoFileContents = () => { - return fs.readJsonAsync(getInfoFilePath()) -} - -const ensureFileInfoContents = () => { - return getInfoFileContents().catch(() => { - debug('could not read info file') - return {} - }) -} - -const writeInfoFileContents = (contents) => { - return fs.outputJsonAsync(getInfoFilePath(), contents, { - spaces: 2, - }) -} - -module.exports = { - clearVersionState, - writeInfoFileContents, - ensureInstallationDir, - ensureFileInfoContents, - getInfoFilePath, - getVerifiedVersion, - getInstallationDir, - getInstalledVersion, - getPathToUserExecutableDir, - getPathToExecutable, - writeInstalledVersion, -} diff --git a/cli/lib/tasks/install.js b/cli/lib/tasks/install.js index 3f1da95214ec..f435d60391e0 100644 --- a/cli/lib/tasks/install.js +++ b/cli/lib/tasks/install.js @@ -1,46 +1,44 @@ const _ = require('lodash') +const os = require('os') const path = require('path') const chalk = require('chalk') +const debug = require('debug')('cypress:cli') const Listr = require('listr') const verbose = require('@cypress/listr-verbose-renderer') -const { stripIndent } = require('common-tags') -const debug = require('debug')('cypress:cli') const Promise = require('bluebird') +const logSymbols = require('log-symbols') +const { stripIndent } = require('common-tags') const fs = require('../fs') const download = require('./download') const util = require('../util') -const info = require('./info') +const state = require('./state') const unzip = require('./unzip') const logger = require('../logger') -const la = require('lazy-ass') -const is = require('check-more-types') +const { throwFormErrorText, errors } = require('../errors') -const alreadyInstalledMsg = (installedVersion, needVersion) => { - logger.log(chalk.yellow(stripIndent` - Cypress ${chalk.cyan(needVersion)} is already installed. Skipping installation. - `)) +const alreadyInstalledMsg = () => { + logger.log(stripIndent` + Skipping installation: + Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway. + `) logger.log() - - logger.log(chalk.gray(stripIndent` - Pass the ${chalk.white('--force')} option if you'd like to reinstall anyway. - `)) } const displayCompletionMsg = () => { - logger.log() // check here to see if we are globally installed if (util.isInstalledGlobally()) { // if we are display a warning + logger.log() logger.warn(stripIndent` - It looks like you\'ve installed Cypress globally. + ${logSymbols.warning} Warning: It looks like you\'ve installed Cypress globally. - This will work, but it'\s not recommended. + This will work, but it'\s not recommended. - The recommended way to install Cypress is as a devDependency per project. + The recommended way to install Cypress is as a devDependency per project. - You should probably run these commands: + You should probably run these commands: - ${chalk.cyan('npm uninstall -g cypress')} - ${chalk.cyan('npm install --save-dev cypress')} @@ -49,134 +47,93 @@ const displayCompletionMsg = () => { return } + logger.log() logger.log( - chalk.yellow('You can now open Cypress by running:'), + 'You can now open Cypress by running:', chalk.cyan(path.join('node_modules', '.bin', 'cypress'), 'open') ) logger.log() - - logger.log( - chalk.yellow('https://on.cypress.io/installing-cypress') - ) + logger.log(chalk.grey('https://on.cypress.io/installing-cypress')) + logger.log() } -const downloadAndUnzip = (version) => { - const options = { - version, +const downloadAndUnzip = ({ version, installDir, downloadDir }) => { + const progress = { + throttle: 100, + onProgress: null, } + const downloadDestination = path.join(downloadDir, 'cypress.zip') + const rendererOptions = getRendererOptions() // let the user know what version of cypress we're downloading! - const message = chalk.yellow(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`) - logger.log(message) + logger.log(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`) logger.log() - const progessify = (task, title) => { - // return higher order function - return (percentComplete, remaining) => { - percentComplete = chalk.white(` ${percentComplete}%`) - - // pluralize seconds remaining - remaining = chalk.gray(`${remaining}s`) - - util.setTaskTitle( - task, - util.titleize(title, percentComplete, remaining), - rendererOptions.renderer - ) - } - } - - // if we are running in CI then use - // the verbose renderer else use - // the default - const rendererOptions = { - renderer: util.isCi() ? verbose : 'default', - } - const tasks = new Listr([ { title: util.titleize('Downloading Cypress'), task: (ctx, task) => { // as our download progresses indicate the status - options.onProgress = progessify(task, 'Downloading Cypress') + progress.onProgress = progessify(task, 'Downloading Cypress') - return download.start(options) - .then(({ filename, downloaded }) => { - // save the download destination for unzipping - ctx.downloadDestination = filename - ctx.downloaded = downloaded - - util.setTaskTitle( - task, - util.titleize(chalk.green('Downloaded Cypress')), - rendererOptions.renderer - ) + return download.start({ version, downloadDestination, progress }) + .then((redirectVersion) => { + if (redirectVersion) version = redirectVersion + debug(`finished downloading file: ${downloadDestination}`) }) - }, - }, - { - title: util.titleize('Unzipping Cypress'), - task: (ctx, task) => { - // as our unzip progresses indicate the status - options.downloadDestination = ctx.downloadDestination - options.onProgress = progessify(task, 'Unzipping Cypress') - - return unzip.start(options) .then(() => { + // save the download destination for unzipping util.setTaskTitle( task, - util.titleize(chalk.green('Unzipped Cypress')), + util.titleize(chalk.green('Downloaded Cypress')), rendererOptions.renderer ) }) }, }, + unzipTask({ + progress, + zipFilePath: downloadDestination, + installDir, + rendererOptions, + }), { title: util.titleize('Finishing Installation'), task: (ctx, task) => { - const { downloadDestination, version } = options - la(is.unemptyString(downloadDestination), 'missing download destination', options) - la(is.bool(ctx.downloaded), 'missing downloaded flag', ctx) - const removeFile = () => { + const cleanup = () => { debug('removing zip file %s', downloadDestination) return fs.removeAsync(downloadDestination) } - const skipFileRemoval = () => { - debug('not removing file %s', downloadDestination) - debug('because it was not downloaded (probably was local file already)') - return Promise.resolve() - } - const cleanup = ctx.downloaded ? removeFile : skipFileRemoval return cleanup() .then(() => { - const dir = info.getPathToUserExecutableDir() - debug('finished installation in', dir) + debug('finished installation in', installDir) util.setTaskTitle( task, - util.titleize(chalk.green('Finished Installation'), chalk.gray(dir)), + util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)), rendererOptions.renderer ) - - return info.writeInstalledVersion(version) }) }, }, ], rendererOptions) // start the tasks! - return tasks.run() + return Promise.resolve(tasks.run()) } const start = (options = {}) => { + + // handle deprecated / removed + if (process.env.CYPRESS_BINARY_VERSION) { + return throwFormErrorText(errors.removed.CYPRESS_BINARY_VERSION)() + } + if (process.env.CYPRESS_SKIP_BINARY_INSTALL) { - logger.log( - chalk.yellow('Skipping binary installation. Env var \'CYPRESS_SKIP_BINARY_INSTALL\' was found.') - ) - return Promise.resolve() + return throwFormErrorText(errors.removed.CYPRESS_SKIP_BINARY_INSTALL)() } debug('installing with options %j', options) @@ -185,94 +142,199 @@ const start = (options = {}) => { force: false, }) - let needVersion = util.pkgVersion() + const pkgVersion = util.pkgVersion() + let needVersion = pkgVersion + debug('version in package.json is', needVersion) - // let this env var reset the binary version we need - if (process.env.CYPRESS_BINARY_VERSION) { - const envVarVersion = process.env.CYPRESS_BINARY_VERSION + // let this environment variable reset the binary version we need + if (process.env.CYPRESS_INSTALL_BINARY) { + + const envVarVersion = process.env.CYPRESS_INSTALL_BINARY + debug('using environment variable CYPRESS_INSTALL_BINARY %s', envVarVersion) + + if (envVarVersion === '0') { + debug('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install') + logger.log( + stripIndent` + ${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`) + logger.log() + return Promise.resolve() + } // if this doesn't match the expected version // then print warning to the user if (envVarVersion !== needVersion) { - debug('using env var CYPRESS_BINARY_VERSION %s', needVersion) - logger.log( - chalk.yellow(stripIndent` - Forcing a binary version different than the default. - - The CLI expected to install version: ${chalk.cyan(needVersion)} - - Instead we will install version: ${chalk.cyan(envVarVersion)} - - Note: there is no guarantee these versions will work properly together. - `) - ) - logger.log('') // reset the version to the env var version needVersion = envVarVersion } } - return info.getInstalledVersion() - .catchReturn(null) - .then((installedVersion) => { - debug('installed version is', installedVersion, 'version needed is', needVersion) + if (process.env.CYPRESS_CACHE_FOLDER) { + const envCache = process.env.CYPRESS_CACHE_FOLDER + logger.log( + stripIndent` + ${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)} - if (options.force) { - return info.clearVersionState() + Previous installs of Cypress may not be found. + `) + logger.log() + } + + const installDir = state.getVersionDir(util.pkgVersion()) + const cacheDir = state.getCacheDir() + + return fs.ensureDirAsync(cacheDir) + .catch({ code: 'EACCES' }, (err) => { + return throwFormErrorText(errors.invalidCacheDirectory)(stripIndent` + Failed to access ${chalk.cyan(cacheDir)}: + + ${err.message} + `) + }) + .then(() => state.getBinaryPkgVersionAsync(installDir)) + .then((binaryVersion) => { + + if (!binaryVersion) { + debug('no binary installed under cli version') + return true } - if (installedVersion === needVersion) { - // our version matches, tell the user this is a noop - alreadyInstalledMsg(installedVersion, needVersion) + // debug('installed version is', binaryVersion, 'version needed is', needVersion) + logger.log(stripIndent` + Cypress ${chalk.green(binaryVersion)} is already installed in ${chalk.cyan(installDir)} + `) + logger.log() - return false + if (options.force) { + debug('performing force install over existing binary') + return true } - if (!installedVersion) { - return info.clearVersionState() + if ((binaryVersion === needVersion) || !util.isSemver(needVersion)) { + // our version matches, tell the user this is a noop + alreadyInstalledMsg() + return false } - logger.warn(stripIndent` - Installed version ${chalk.cyan(`(${installedVersion})`)} does not match needed version ${chalk.cyan(`(${needVersion})`)}. - `) - logger.log() + return true }) - .then((ret) => { + .then((shouldInstall) => { // noop if we've been told not to download - if (ret === false) { + if (!shouldInstall) { + debug('Not downloading or installing binary') return } - // TODO: what to do about this? let's just not support it - // let needVersion be a path to a real cypress binary - // instead of a version we download from the internet - return fs.statAsync(needVersion) - .then(() => { - logger.log('Installing local Cypress binary from %s', needVersion) + if (needVersion !== pkgVersion) { + logger.log( + chalk.yellow(stripIndent` + ${logSymbols.warning} Warning: Forcing a binary version different than the default. - // TODO: move all this shit, it doesn't work as is now anyway - return unzip.start({ - zipDestination: needVersion, - destination: info.getInstallationDir(), - executable: info.getPathToUserExecutableDir(), - }) - .then(() => info.writeInstalledVersion('unknown')) - }) - .catch(() => { - debug('preparing to download and unzip version', needVersion) + The CLI expected to install version: ${chalk.green(pkgVersion)} - return downloadAndUnzip(needVersion) - .then(() => { - // wait 1 second for a good user experience - return Promise.delay(1000) + Instead we will install version: ${chalk.green(needVersion)} + + These versions may not work properly together. + `) + ) + logger.log() + } + + // see if version supplied is a path to a binary + return fs.pathExistsAsync(needVersion) + .then((exists) => { + if (exists) { + return path.extname(needVersion) === '.zip' ? needVersion : false + } + + const possibleFile = util.formAbsolutePath(needVersion) + debug('checking local file', possibleFile, 'cwd', process.cwd()) + + return fs.pathExistsAsync(possibleFile) + .then((exists) => { + // if this exists return the path to it + // else false + if (exists && path.extname(possibleFile) === '.zip') { + return possibleFile + } + return false }) - .then(displayCompletionMsg) }) + .then((pathToLocalFile) => { + if (pathToLocalFile) { + debug('found local file at', needVersion) + debug('skipping download') + + const rendererOptions = getRendererOptions() + return new Listr([unzipTask({ + progress: { + throttle: 100, + onProgress: null, + }, + zipFilePath: needVersion, + installDir, + rendererOptions, + })], rendererOptions).run() + } + + if (options.force) { + debug('Cypress already installed at', installDir) + debug('but the installation was forced') + } + + debug('preparing to download and unzip version ', needVersion, 'to path', installDir) + + const downloadDir = os.tmpdir() + return downloadAndUnzip({ version: needVersion, installDir, downloadDir }) + }) + // delay 1 sec for UX, unless we are testing + .then(() => Promise.delay(1000)) + .then(displayCompletionMsg) }) } module.exports = { start, } + +const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }) => ({ + title: util.titleize('Unzipping Cypress'), + task: (ctx, task) => { + // as our unzip progresses indicate the status + progress.onProgress = progessify(task, 'Unzipping Cypress') + + return unzip.start({ zipFilePath, installDir, progress }) + .then(() => { + util.setTaskTitle( + task, + util.titleize(chalk.green('Unzipped Cypress')), + rendererOptions.renderer + ) + }) + }, +}) + +const progessify = (task, title) => { + // return higher order function + return (percentComplete, remaining) => { + percentComplete = chalk.white(` ${percentComplete}%`) + + // pluralize seconds remaining + remaining = chalk.gray(`${remaining}s`) + + util.setTaskTitle( + task, + util.titleize(title, percentComplete, remaining), + getRendererOptions().renderer + ) + } +} + +// if we are running in CI then use +// the verbose renderer else use +// the default +const getRendererOptions = () => ({ + renderer: util.isCi() ? verbose : 'default', +}) diff --git a/cli/lib/tasks/state.js b/cli/lib/tasks/state.js new file mode 100644 index 000000000000..73c92e1bde77 --- /dev/null +++ b/cli/lib/tasks/state.js @@ -0,0 +1,143 @@ +const _ = require('lodash') +const os = require('os') +const path = require('path') +const debug = require('debug')('cypress:cli') +const cachedir = require('cachedir') + +const fs = require('../fs') +const util = require('../util') + +const getPlatformExecutable = () => { + const platform = os.platform() + switch (platform) { + case 'darwin': return 'Contents/MacOS/Cypress' + case 'linux': return 'Cypress' + case 'win32': return 'Cypress.exe' + // TODO handle this error using our standard + default: throw new Error(`Platform: "${platform}" is not supported.`) + } +} + +const getPlatFormBinaryFolder = () => { + const platform = os.platform() + switch (platform) { + case 'darwin': return 'Cypress.app' + case 'linux': return 'Cypress' + case 'win32': return 'Cypress' + // TODO handle this error using our standard + default: throw new Error(`Platform: "${platform}" is not supported.`) + } +} + +const getBinaryPkgPath = (binaryDir) => { + const platform = os.platform() + switch (platform) { + case 'darwin': return path.join(binaryDir, 'Contents', 'Resources', 'app', 'package.json') + case 'linux': return path.join(binaryDir, 'resources', 'app', 'package.json') + case 'win32': return path.join(binaryDir, 'resources', 'app', 'package.json') + // TODO handle this error using our standard + default: throw new Error(`Platform: "${platform}" is not supported.`) + } +} + +/** + * Get path to binary directory +*/ +const getBinaryDir = (version = util.pkgVersion()) => { + return path.join(getVersionDir(version), getPlatFormBinaryFolder()) +} + +const getVersionDir = (version = util.pkgVersion()) => { + return path.join(getCacheDir(), version) +} + +const getCacheDir = () => { + let cache_directory = cachedir('Cypress') + if (process.env.CYPRESS_CACHE_FOLDER) { + const envVarCacheDir = process.env.CYPRESS_CACHE_FOLDER + debug('using environment variable CYPRESS_CACHE_FOLDER %s', envVarCacheDir) + cache_directory = envVarCacheDir + } + return cache_directory +} + +const parsePlatformBinaryFolder = (executable) => { + if (!executable.toString().endsWith(getPlatformExecutable())) { + return false + } + if (os.platform() === 'darwin') { + return path.resolve(executable, '..', '..', '..') + } + return path.resolve(executable, '..') +} + +const getDistDir = () => { + return path.join(__dirname, '..', '..', 'dist') +} + +const getBinaryStatePath = (binaryDir) => { + return path.join(binaryDir, 'binary_state.json') +} + +const getBinaryStateContentsAsync = (binaryDir) => { + return fs.readJsonAsync(getBinaryStatePath(binaryDir)) + .catch({ code: 'ENOENT' }, SyntaxError, () => { + debug('could not read binary_state.json file') + return {} + }) +} + +const getBinaryVerifiedAsync = (binaryDir) => { + return getBinaryStateContentsAsync(binaryDir) + .tap(debug) + .get('verified') +} + +const clearBinaryStateAsync = (binaryDir) => { + return fs.removeAsync(getBinaryStatePath(binaryDir)) +} + +/** + * @param {boolean} verified + */ +const writeBinaryVerifiedAsync = (verified, binaryDir) => { + return getBinaryStateContentsAsync(binaryDir) + .then((contents) => { + return fs.outputJsonAsync( + getBinaryStatePath(binaryDir), + _.extend(contents, { verified }), + { spaces: 2 }) + }) +} + +const getPathToExecutable = (binaryDir) => { + return path.join(binaryDir, getPlatformExecutable()) +} + +const getBinaryPkgVersionAsync = (binaryDir) => { + const pathToPackageJson = getBinaryPkgPath(binaryDir) + return fs.pathExistsAsync(pathToPackageJson) + .then((exists) => { + if (!exists) { + return null + } + return fs.readJsonAsync(pathToPackageJson) + .get('version') + }) +} + + +module.exports = { + getPathToExecutable, + getPlatformExecutable, + getBinaryPkgVersionAsync, + getBinaryVerifiedAsync, + getBinaryPkgPath, + getBinaryDir, + getCacheDir, + clearBinaryStateAsync, + writeBinaryVerifiedAsync, + parsePlatformBinaryFolder, + getDistDir, + getVersionDir, +} diff --git a/cli/lib/tasks/unzip.js b/cli/lib/tasks/unzip.js index 7c253cc177c1..8b443672ce99 100644 --- a/cli/lib/tasks/unzip.js +++ b/cli/lib/tasks/unzip.js @@ -1,4 +1,5 @@ -const _ = require('lodash') +const la = require('lazy-ass') +const is = require('check-more-types') const cp = require('child_process') const os = require('os') const yauzl = require('yauzl') @@ -10,130 +11,131 @@ const readline = require('readline') const { throwFormErrorText, errors } = require('../errors') const fs = require('../fs') const util = require('../util') -const info = require('./info') // expose this function for simple testing -const unzip = (options = {}) => { - _.defaults(options, { - downloadDestination: null, - onProgress: () => {}, - zipDestination: info.getInstallationDir(), - }) - - const { downloadDestination, zipDestination } = options +const unzip = ({ zipFilePath, installDir, progress }) => { - debug('unzipping from %s', downloadDestination) - debug('into %s', zipDestination) + debug('unzipping from %s', zipFilePath) + debug('into', installDir) - if (!downloadDestination) { + if (!zipFilePath) { throw new Error('Missing zip filename') } - return new Promise((resolve, reject) => { - return yauzl.open(downloadDestination, (err, zipFile) => { - if (err) return reject(err) + return fs.ensureDirAsync(installDir) + .then(() => { + return new Promise((resolve, reject) => { + return yauzl.open(zipFilePath, (err, zipFile) => { + if (err) return reject(err) + // debug('zipfile.paths:', zipFile) + // zipFile.on('entry', debug) + // debug(zipFile.readEntry()) + const total = zipFile.entryCount - const total = zipFile.entryCount + debug('zipFile entries count', total) - debug('zipFile entries count', total) - const started = new Date() + const started = new Date() - let percent = 0 - let count = 0 + let percent = 0 + let count = 0 - const notify = (percent) => { - const elapsed = new Date() - started + const notify = (percent) => { + const elapsed = +new Date() - +started - const eta = util.calculateEta(percent, elapsed) + const eta = util.calculateEta(percent, elapsed) - options.onProgress(percent, util.secsRemaining(eta)) - } + progress.onProgress(percent, util.secsRemaining(eta)) + } - const tick = () => { - count += 1 + const tick = () => { + count += 1 - percent = ((count / total) * 100).toFixed(0) + percent = ((count / total) * 100) + const displayPercent = percent.toFixed(0) - return notify(percent) - } + return notify(displayPercent) + } - const unzipWithNode = () => { - const endFn = (err) => { - if (err) { return reject(err) } + const unzipWithNode = () => { + const endFn = (err) => { + if (err) { return reject(err) } - return resolve() - } + return resolve() + } - const obj = { - dir: zipDestination, - onEntry: tick, - } + const opts = { + dir: installDir, + onEntry: tick, + } - return extract(downloadDestination, obj, endFn) - } + return 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 unzipWithOsx = () => { - const copyingFileRe = /^copying file/ + //# 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 = () => { + const copyingFileRe = /^copying file/ - const sp = cp.spawn('ditto', ['-xkV', downloadDestination, zipDestination]) - sp.on('error', () => + const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]) + sp.on('error', () => // f-it just unzip with node - unzipWithNode() - ) + unzipWithNode() + ) - sp.on('close', (code) => { - if (code === 0) { + sp.on('close', (code) => { + if (code === 0) { // make sure we get to 100% on the progress bar // because reading in lines is not really accurate - percent = 100 - notify(percent) - - return resolve() - } - - return unzipWithNode() - }) + percent = 100 + notify(percent) + + return resolve() + } + + return unzipWithNode() + }) + + return readline.createInterface({ + input: sp.stderr, + }) + .on('line', (line) => { + if (copyingFileRe.test(line)) { + return tick() + } + }) + } - return readline.createInterface({ - input: sp.stderr, - }) - .on('line', (line) => { - if (copyingFileRe.test(line)) { - return tick() - } - }) - } - - switch (os.platform()) { - case 'darwin': - return unzipWithOsx() - case 'linux': - case 'win32': - return unzipWithNode() - default: - return - } + switch (os.platform()) { + case 'darwin': + return unzipWithOsx() + case 'linux': + case 'win32': + return unzipWithNode() + default: + return + } + }) }) }) } -const start = (options = {}) => { - const dir = info.getPathToUserExecutableDir() - - debug('removing existing unzipped directory', dir) +const start = ({ zipFilePath, installDir, progress }) => { + la(is.unemptyString(installDir), 'missing installDir') + if (!progress) progress = { onProgress: () => ({}) } - // blow away the executable if its found - // and dont worry about errors from remove - return fs.removeAsync(dir) - .catchReturn(null) + return fs.pathExists(installDir) + .then((exists) => { + if (exists) { + debug('removing existing unzipped binary', installDir) + return fs.removeAsync(installDir) + } + }) .then(() => { - return unzip(options) + return unzip({ zipFilePath, installDir, progress }) }) .catch(throwFormErrorText(errors.failedUnzip)) } @@ -141,13 +143,3 @@ const start = (options = {}) => { module.exports = { start, } - -// demo / test -if (!module.parent && process.env.ZIP) { - /* eslint-disable no-console */ - console.log('unzipping file', process.env.ZIP) - start({ - downloadDestination: process.env.ZIP, - }).catch(console.error) - /* eslint-enable no-console */ -} diff --git a/cli/lib/tasks/verify.js b/cli/lib/tasks/verify.js index d71e7a0454c0..fb2835db00bc 100644 --- a/cli/lib/tasks/verify.js +++ b/cli/lib/tasks/verify.js @@ -6,15 +6,15 @@ const debug = require('debug')('cypress:cli') const verbose = require('@cypress/listr-verbose-renderer') const { stripIndent } = require('common-tags') const Promise = require('bluebird') +const logSymbols = require('log-symbols') + const { throwFormErrorText, errors } = require('../errors') const fs = require('../fs') const util = require('../util') const logger = require('../logger') const xvfb = require('../exec/xvfb') -const info = require('./info') - -const differentFrom = (a, b) => a !== b +const state = require('./state') const verificationError = (message) => { return _.extend(new Error(''), { name: '', message, isVerificationError: true }) @@ -24,41 +24,24 @@ const xvfbError = (message) => { return _.extend(new Error(''), { name: '', message, isXvfbError: true }) } -const checkIfNotInstalledOrMissingExecutable = (installedVersion, executable) => { +const isMissingExecutable = (binaryDir) => { + const executable = state.getPathToExecutable(binaryDir) debug('checking if executable exists', executable) - - return fs.statAsync(executable) - .then(() => { - // after verifying its physically accessible - // we can now check that its installed in info.json - if (!installedVersion) { - throw new Error() - } - }) - .catch(() => { - // bail if we don't have an installed version - // because its physically missing or its - // not in info.json - return throwFormErrorText(errors.missingApp)(stripIndent` + return fs.pathExistsAsync(executable) + .then((exists) => { + if (!exists) { + return throwFormErrorText(errors.missingApp(binaryDir))(stripIndent` Cypress executable not found at: ${chalk.cyan(executable)} `) + } }) } -const writeVerifiedVersion = (verifiedVersion) => { - debug('writing verified version string "%s"', verifiedVersion) - - return info.ensureFileInfoContents() - .then((contents) => { - return info.writeInfoFileContents(_.extend(contents, { verifiedVersion })) - }) -} - -const runSmokeTest = () => { +const runSmokeTest = (binaryDir) => { debug('running smoke test') let stderr = '' let stdout = '' - const cypressExecPath = info.getPathToExecutable() + const cypressExecPath = state.getPathToExecutable(binaryDir) debug('using Cypress executable %s', cypressExecPath) // TODO switch to execa for this? @@ -124,17 +107,13 @@ const runSmokeTest = () => { } } -function testBinary (version) { +function testBinary (version, binaryDir) { debug('running binary verification check', version) - const dir = info.getPathToUserExecutableDir() - // let the user know what version of cypress we're downloading! - logger.log( - chalk.yellow( - `It looks like this is your first time using Cypress: ${chalk.cyan(version)}` - ) - ) + logger.log(stripIndent` + It looks like this is your first time using Cypress: ${chalk.cyan(version)} + `) logger.log() @@ -148,25 +127,26 @@ function testBinary (version) { const tasks = new Listr([ { - title: util.titleize('Verifying Cypress can run', chalk.gray(dir)), + title: util.titleize('Verifying Cypress can run', chalk.gray(binaryDir)), task: (ctx, task) => { - // clear out the verified version - return writeVerifiedVersion(null) + debug('clearing out the verified version') + return state.clearBinaryStateAsync(binaryDir) .then(() => { return Promise.all([ - runSmokeTest(), - Promise.delay(1500), // good user experience + runSmokeTest(binaryDir), + Promise.resolve().delay(1500), // good user experience ]) }) .then(() => { - return writeVerifiedVersion(version) + debug('write verified: true') + return state.writeBinaryVerifiedAsync(true, binaryDir) }) .then(() => { util.setTaskTitle( task, util.titleize( chalk.green('Verified Cypress!'), - chalk.gray(dir) + chalk.gray(binaryDir) ), rendererOptions.renderer ) @@ -180,22 +160,25 @@ function testBinary (version) { return tasks.run() } -const maybeVerify = (installedVersion, options = {}) => { - return info.getVerifiedVersion() - .then((verifiedVersion) => { - debug('has verified version', verifiedVersion) +const maybeVerify = (installedVersion, binaryDir, options = {}) => { + return state.getBinaryVerifiedAsync(binaryDir) + .then((isVerified) => { - // verify if packageVersion and verifiedVersion are different - const shouldVerify = options.force || differentFrom(installedVersion, verifiedVersion) + debug('is Verified ?', isVerified) - debug('run verification check?', shouldVerify) + let shouldVerify = !isVerified + // force verify if options.force + if (options.force) { + debug('force verify') + shouldVerify = true + } if (shouldVerify) { - return testBinary(installedVersion) + return testBinary(installedVersion, binaryDir) .then(() => { if (options.welcomeMessage) { logger.log() - logger.warn('Opening Cypress...') + logger.log('Opening Cypress...') } }) } @@ -206,39 +189,88 @@ const start = (options = {}) => { debug('verifying Cypress app') const packageVersion = util.pkgVersion() + let binaryDir = state.getBinaryDir(packageVersion) _.defaults(options, { force: false, welcomeMessage: true, }) - return info.getInstalledVersion() - .then((installedVersion) => { - debug('installed version is', installedVersion, 'comparing to', packageVersion) + const checkEnvVar = () => { + debug('checking environment variables') + if (process.env.CYPRESS_RUN_BINARY) { + const envBinaryPath = process.env.CYPRESS_RUN_BINARY + debug('CYPRESS_RUN_BINARY exists, =', envBinaryPath) + logger.log(stripIndent` + ${chalk.yellow('Note:')} You have set the environment variable: ${chalk.white('CYPRESS_RUN_BINARY=')}${chalk.cyan(envBinaryPath)}: + + This overrides the default Cypress binary path used. + `) + logger.log() - // figure out where this executable is supposed to be at - const executable = info.getPathToExecutable() + return util.isExecutableAsync(envBinaryPath) + .then((isExecutable) => { + debug('CYPRESS_RUN_BINARY is executable? :', isExecutable) + if (!isExecutable) { + return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(stripIndent` + The supplied binary path is not executable + `) + } + }) + .then(() => fs.realpathAsync(envBinaryPath)) + .then((realPath) => { + debug('CYPRESS_RUN_BINARY has realPath:', realPath) + const envBinaryDir = state.parsePlatformBinaryFolder(realPath) + if (!envBinaryDir) { + return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))() + } + debug('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir) - return checkIfNotInstalledOrMissingExecutable(installedVersion, executable) - .return(installedVersion) - }) - .then((installedVersion) => { - if (installedVersion !== packageVersion) { - // warn if we installed with CYPRESS_BINARY_VERSION or changed version + binaryDir = envBinaryDir + }) + .catch({ code: 'ENOENT' }, (err) => { + return throwFormErrorText(errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath))(err.message) + }) + } + return Promise.resolve() + } + + + return checkEnvVar() + .then(() => isMissingExecutable(binaryDir)) + .tap(() => debug('binaryDir is ', binaryDir)) + .then(() => state.getBinaryPkgVersionAsync(binaryDir)) + .then((binaryVersion) => { + + if (!binaryVersion) { + debug('no Cypress binary found for cli version ', packageVersion) + return throwFormErrorText(errors.missingApp(binaryDir))(` + Cannot read binary version from: ${chalk.cyan(state.getBinaryPkgPath(binaryDir))} + `) + } + + debug(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`) + + if (binaryVersion !== packageVersion) { + // warn if we installed with CYPRESS_INSTALL_BINARY or changed version // in the package.json - const msg = stripIndent` - Installed version ${chalk.cyan(installedVersion)} does not match the expected package version ${chalk.cyan(packageVersion)} + logger.log(`Found binary version ${chalk.green(binaryVersion)} installed in: ${chalk.cyan(binaryDir)}`) + logger.log() + logger.warn(stripIndent` + + + ${logSymbols.warning} Warning: Binary version ${chalk.green(binaryVersion)} does not match the expected package version ${chalk.green(packageVersion)} - Note: there is no guarantee these versions will work properly together. - ` + These versions may not work properly together. + `) - logger.warn(msg) logger.log() } - return maybeVerify(installedVersion, options) + return maybeVerify(binaryVersion, binaryDir, options) }) + .catch((err) => { if (err.known) { throw err diff --git a/cli/lib/util.js b/cli/lib/util.js index fe45ad4ab34d..adcbb2529527 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -1,13 +1,20 @@ const _ = require('lodash') const R = require('ramda') +const os = require('os') +const tty = require('tty') const path = require('path') const isCi = require('is-ci') +const getos = require('getos') const chalk = require('chalk') +const Promise = require('bluebird') +const executable = require('executable') const supportsColor = require('supports-color') const isInstalledGlobally = require('is-installed-globally') const pkg = require(path.join(__dirname, '..', 'package.json')) const logger = require('./logger') +const debug = require('debug')('cypress:cli') +const getosAsync = Promise.promisify(getos) const joinWithEq = (x, y) => `${x}=${y}` // converts an object (single level) into @@ -36,17 +43,80 @@ function stdoutLineMatches (expectedLine, stdout) { return lines.some(lineMatches) } +/** + * Prints NODE_OPTIONS using debug() module, but only + * if DEBUG=cypress... is set + */ +function printNodeOptions (log = debug) { + if (!log.enabled) { + return + } + + if (process.env.NODE_OPTIONS) { + log('NODE_OPTIONS=%s', process.env.NODE_OPTIONS) + } else { + log('NODE_OPTIONS is not set') + } +} + const util = { normalizeModuleOptions, + printNodeOptions, + isCi () { return isCi }, + getEnvOverrides () { + return _ + .chain({}) + .extend(util.getEnvColors()) + .extend(util.getForceTty()) + .omitBy(_.isUndefined) // remove undefined values + .mapValues((value) => { // stringify to 1 or 0 + return value ? '1' : '0' + }) + .value() + }, + + getForceTty () { + return { + FORCE_STDIN_TTY: util.isTty(process.stdin.fd), + FORCE_STDOUT_TTY: util.isTty(process.stdout.fd), + FORCE_STDERR_TTY: util.isTty(process.stderr.fd), + } + }, + + getEnvColors () { + const sc = util.supportsColor() + + return { + FORCE_COLOR: sc, + DEBUG_COLORS: sc, + MOCHA_COLORS: sc ? true : undefined, + } + }, + + isTty (fd) { + return tty.isatty(fd) + }, + supportsColor () { - // we only care about stderr supporting color - // since thats what our DEBUG logs use - return Boolean(supportsColor.stderr) + // if we've been explictly told not to support + // color then turn this off + if (process.env.NO_COLOR) { + return false + } + + // https://github.com/cypress-io/cypress/issues/1747 + // always return true in CI providers + if (process.env.CI) { + return true + } + + // ensure that both stdout and stderr support color + return Boolean(supportsColor.stdout) && Boolean(supportsColor.stderr) }, cwd () { @@ -108,6 +178,37 @@ const util = { return isInstalledGlobally }, + isSemver (str) { + return /^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str) + }, + + isExecutableAsync (filePath) { + return Promise.resolve(executable(filePath)) + }, + + getOsVersionAsync () { + return Promise.try(() => { + if (os.platform() === 'linux') { + return getosAsync() + .then((osInfo) => [osInfo.dist, osInfo.release].join(' - ')) + .catch(() => os.release()) + } else { + return os.release() + } + }) + }, + + // attention: + // when passing relative path to NPM post install hook, the current working + // directory is set to the `node_modules/cypress` folder + // the user is probably passing relative path with respect to root package folder + formAbsolutePath (filename) { + if (path.isAbsolute(filename)) { + return filename + } + return path.join(process.cwd(), '..', '..', filename) + }, + stdoutLineMatches, } diff --git a/cli/package.json b/cli/package.json index 02571cfe5628..0b00f82c9ad3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -12,26 +12,34 @@ "scripts": { "test": "npm run test-unit", "test-unit": "npm run dtslint && npm run unit", - "test-watch": "bin-up mocha --watch", + "test-watch": "npm run unit -- --watch", "test-dependencies": "bin-up deps-ok && dependency-check . --no-dev", "test-debug": "node --inspect --debug-brk $(bin-up _mocha)", + "test-cov": "nyc npm run unit", + "unit": "BLUEBIRD_DEBUG=1 NODE_ENV=test bin-up mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json", "lint": "bin-up eslint --fix *.js bin/* lib/*.js lib/**/*.js test/*.js test/**/*.js", "dtslint": "dtslint types", "prebuild": "npm run test-dependencies && node ./scripts/start-build.js", "build": "node ./scripts/build.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}\";", - "unit": "bin-up mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json" + "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" + }, + "nyc": { + "exclude": [ + "test", + "scripts" + ] }, "types": "types", "dependencies": { "@cypress/listr-verbose-renderer": "0.4.1", - "@cypress/xvfb": "1.1.3", + "@cypress/xvfb": "1.2.3", "@types/blob-util": "1.3.3", "@types/bluebird": "3.5.18", "@types/chai": "4.0.8", "@types/chai-jquery": "1.1.35", + "@types/chalk": "2.2.0", "@types/jquery": "3.2.16", "@types/lodash": "4.14.87", "@types/minimatch": "3.0.1", @@ -39,11 +47,13 @@ "@types/sinon": "4.0.0", "@types/sinon-chai": "2.7.29", "bluebird": "3.5.0", + "cachedir": "1.2.0", "chalk": "2.1.0", "check-more-types": "2.24.0", "commander": "2.11.0", "common-tags": "1.4.0", "debug": "3.1.0", + "executable": "4.1.1", "extract-zip": "1.6.6", "fs-extra": "4.0.1", "getos": "2.8.4", @@ -53,6 +63,7 @@ "lazy-ass": "1.6.0", "listr": "0.12.0", "lodash": "4.17.4", + "log-symbols": "2.2.0", "minimist": "1.2.0", "progress": "1.1.8", "ramda": "0.24.1", @@ -76,9 +87,11 @@ "dtslint": "0.2.0", "execa-wrap": "1.1.0", "nock": "^9.0.9", + "nyc": "11.7.1", + "proxyquire": "2.0.1", "shelljs": "0.7.8", - "sinon": "3.2.1", - "snap-shot-it": "4.0.1", + "sinon": "5.0.7", + "snap-shot-it": "^5.0.0", "strip-ansi": "4.0.0" }, "files": [ diff --git a/cli/scripts/build.js b/cli/scripts/build.js index 4f47fe673b60..cd2f566e742d 100644 --- a/cli/scripts/build.js +++ b/cli/scripts/build.js @@ -13,6 +13,7 @@ const { license, bugs, repository, + keywords, } = require('@packages/root') // the rest of properties should come from the package.json in CLI folder @@ -33,6 +34,7 @@ function preparePackageForNpmRelease (json) { license, bugs, repository, + keywords, types: 'types', // typescript types scripts: { postinstall: 'node index.js --exec install', diff --git a/cli/test/.eslintrc b/cli/test/.eslintrc index 4a5213767793..9714ee5390f8 100644 --- a/cli/test/.eslintrc +++ b/cli/test/.eslintrc @@ -1,6 +1,7 @@ { "globals": { - "lib": true + "lib": true, + "sinon": true }, "extends": [ "plugin:cypress-dev/tests" diff --git a/cli/test/lib/build_spec.js b/cli/test/lib/build_spec.js index 3e42493bfa42..f2b3367748f8 100644 --- a/cli/test/lib/build_spec.js +++ b/cli/test/lib/build_spec.js @@ -20,11 +20,11 @@ describe('package.json build', () => { // stub package.json in CLI // with a few test props // the rest should come from root package.json file - this.sandbox.stub(fs, 'readJsonAsync').resolves({ + sinon.stub(fs, 'readJsonAsync').resolves({ name: 'test', engines: 'test engines', }) - this.sandbox.stub(fs, 'outputJsonAsync').resolves() + sinon.stub(fs, 'outputJsonAsync').resolves() }) it('author name and version', () => { diff --git a/cli/test/lib/cli_spec.js b/cli/test/lib/cli_spec.js index 10329b170fbe..32ab10840b69 100644 --- a/cli/test/lib/cli_spec.js +++ b/cli/test/lib/cli_spec.js @@ -5,7 +5,7 @@ const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) const run = require(`${lib}/exec/run`) const open = require(`${lib}/exec/open`) -const info = require(`${lib}/tasks/info`) +const state = require(`${lib}/tasks/state`) const verify = require(`${lib}/tasks/verify`) const install = require(`${lib}/tasks/install`) const snapshot = require('snap-shot-it') @@ -16,12 +16,27 @@ describe('cli', function () { beforeEach(function () { logger.reset() - this.sandbox.stub(process, 'exit') - this.sandbox.stub(util, 'exit') - this.sandbox.stub(util, 'logErrorExit1') + sinon.stub(process, 'exit') + sinon.stub(util, 'exit') + sinon.stub(util, 'logErrorExit1') this.exec = (args) => cli.init(`node test ${args}`.split(' ')) }) + context('unknown option', () => { + // note it shows help for that specific command + it('shows help', () => + execa('bin/cypress', ['open', '--foo']).then((result) => { + snapshot('shows help for open --foo', result) + }) + ) + + it('shows help for run command', () => + execa('bin/cypress', ['run', '--foo']).then((result) => { + snapshot('shows help for run --foo', result) + }) + ) + }) + context('help command', () => { it('shows help', () => execa('bin/cypress', ['help']).then(snapshot) @@ -44,8 +59,8 @@ describe('cli', function () { context('cypress version', function () { it('reports package version', function (done) { - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(info, 'getInstalledVersion').resolves('X.Y.Z') + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('X.Y.Z') this.exec('version') process.exit.callsFake(() => { @@ -55,8 +70,8 @@ describe('cli', function () { }) it('reports package and binary message', function (done) { - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(info, 'getInstalledVersion').resolves('X.Y.Z') + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('X.Y.Z') this.exec('version') process.exit.callsFake(() => { @@ -66,8 +81,8 @@ describe('cli', function () { }) it('handles non-existent binary version', function (done) { - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(info, 'getInstalledVersion').resolves(null) + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(null) this.exec('version') process.exit.callsFake(() => { @@ -77,8 +92,8 @@ describe('cli', function () { }) it('handles non-existent binary --version', function (done) { - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(info, 'getInstalledVersion').resolves(null) + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(null) this.exec('--version') process.exit.callsFake(() => { @@ -88,8 +103,8 @@ describe('cli', function () { }) it('handles non-existent binary -v', function (done) { - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(info, 'getInstalledVersion').resolves(null) + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(null) this.exec('-v') process.exit.callsFake(() => { @@ -101,7 +116,7 @@ describe('cli', function () { context('cypress run', function () { beforeEach(function () { - this.sandbox.stub(run, 'start').resolves(0) + sinon.stub(run, 'start').resolves(0) }) it('calls run.start with options + exits with code', function (done) { @@ -125,21 +140,6 @@ describe('cli', function () { }) }) - it('calls run without group flag', function () { - this.exec('run') - expect(run.start).to.be.calledWith({}) - }) - - it('calls run with group flag', function () { - this.exec('run --group') - expect(run.start).to.be.calledWith({ group: true }) - }) - - it('calls run with groupId', function () { - this.exec('run --group-id foo') - expect(run.start).to.be.calledWith({ groupId: 'foo' }) - }) - it('calls run with port', function () { this.exec('run --port 7878') expect(run.start).to.be.calledWith({ port: '7878' }) @@ -194,11 +194,12 @@ describe('cli', function () { this.exec('run --headed') expect(run.start).to.be.calledWith({ headed: true }) }) + }) context('cypress open', function () { beforeEach(function () { - this.sandbox.stub(open, 'start').resolves(0) + sinon.stub(open, 'start').resolves(0) }) it('calls open.start with relative --project folder', function () { @@ -212,13 +213,13 @@ describe('cli', function () { }) it('calls open.start with options', function () { - // this.sandbox.stub(open, 'start').resolves() + // sinon.stub(open, 'start').resolves() this.exec('open --port 7878') expect(open.start).to.be.calledWith({ port: '7878' }) }) it('calls open.start with global', function () { - // this.sandbox.stub(open, 'start').resolves() + // sinon.stub(open, 'start').resolves() this.exec('open --port 7878 --global') expect(open.start).to.be.calledWith({ port: '7878', global: true }) }) @@ -237,16 +238,22 @@ describe('cli', function () { }) - it('install calls install.start with force: true', function () { - this.sandbox.stub(install, 'start').resolves() + it('install calls install.start without forcing', function () { + sinon.stub(install, 'start').resolves() this.exec('install') + expect(install.start).not.to.be.calledWith({ force: true }) + }) + + it('install calls install.start with force: true when passed', function () { + sinon.stub(install, 'start').resolves() + this.exec('install --force') expect(install.start).to.be.calledWith({ force: true }) }) it('install calls install.start + catches errors', function (done) { const err = new Error('foo') - this.sandbox.stub(install, 'start').rejects(err) + sinon.stub(install, 'start').rejects(err) this.exec('install') util.logErrorExit1.callsFake((e) => { @@ -254,22 +261,25 @@ describe('cli', function () { done() }) }) + context('cypress verify', function () { - it('verify calls verify.start with force: true', function () { - this.sandbox.stub(verify, 'start').resolves() - this.exec('verify') - expect(verify.start).to.be.calledWith({ force: true, welcomeMessage: false }) - }) - it('verify calls verify.start + catches errors', function (done) { - const err = new Error('foo') + it('verify calls verify.start with force: true', function () { + sinon.stub(verify, 'start').resolves() + this.exec('verify') + expect(verify.start).to.be.calledWith({ force: true, welcomeMessage: false }) + }) - this.sandbox.stub(verify, 'start').rejects(err) - this.exec('verify') + it('verify calls verify.start + catches errors', function (done) { + const err = new Error('foo') - util.logErrorExit1.callsFake((e) => { - expect(e).to.eq(err) - done() + sinon.stub(verify, 'start').rejects(err) + this.exec('verify') + + util.logErrorExit1.callsFake((e) => { + expect(e).to.eq(err) + done() + }) }) }) }) diff --git a/cli/test/lib/cypress_spec.js b/cli/test/lib/cypress_spec.js index d1baa5b705ee..9910e5d5df0f 100644 --- a/cli/test/lib/cypress_spec.js +++ b/cli/test/lib/cypress_spec.js @@ -14,10 +14,32 @@ const cypress = require(`${lib}/cypress`) describe('cypress', function () { context('.open', function () { + beforeEach(function () { + sinon.stub(open, 'start').resolves() + }) + + const getCallArgs = R.path(['lastCall', 'args', 0]) + const getStartArgs = () => { + expect(open.start).to.be.called + return getCallArgs(open.start) + } + it('calls open#start, passing in options', function () { - this.sandbox.stub(open, 'start').resolves() cypress.open({ foo: 'foo' }) - expect(open.start).to.be.calledWith({ foo: 'foo' }) + .then(getStartArgs) + .then((args) => { + expect(args.foo).to.equal('foo') + }) + }) + + it('normalizes config object', () => { + const config = { + pageLoadTime: 10000, + watchForFileChanges: false, + } + cypress.open({ config }) + .then(getStartArgs) + .then(snapshot) }) }) @@ -25,8 +47,8 @@ describe('cypress', function () { let outputPath beforeEach(function () { outputPath = path.join(os.tmpdir(), 'cypress/monorepo/cypress_spec/output.json') - this.sandbox.stub(tmp, 'fileAsync').resolves(outputPath) - this.sandbox.stub(run, 'start').resolves() + sinon.stub(tmp, 'fileAsync').resolves(outputPath) + sinon.stub(run, 'start').resolves() return fs.outputJsonAsync(outputPath, { code: 0, failingTests: [], diff --git a/cli/test/lib/errors_spec.js b/cli/test/lib/errors_spec.js index 21630ba2f85d..f12aa57068bc 100644 --- a/cli/test/lib/errors_spec.js +++ b/cli/test/lib/errors_spec.js @@ -9,9 +9,8 @@ describe('errors', function () { const { missingXvfb } = errors beforeEach(function () { - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(os, 'platform').returns('test platform') - this.sandbox.stub(os, 'release').returns('test release') + sinon.stub(util, 'pkgVersion').returns('1.2.3') + os.platform.returns('test platform') }) describe('individual', () => { diff --git a/cli/test/lib/exec/open_spec.js b/cli/test/lib/exec/open_spec.js index 5de43c181016..f91844184dc7 100644 --- a/cli/test/lib/exec/open_spec.js +++ b/cli/test/lib/exec/open_spec.js @@ -8,9 +8,9 @@ const util = require(`${lib}/util`) describe('exec open', function () { context('.start', function () { beforeEach(function () { - this.sandbox.stub(util, 'isInstalledGlobally').returns(true) - this.sandbox.stub(verify, 'start').resolves() - this.sandbox.stub(spawn, 'start').resolves() + sinon.stub(util, 'isInstalledGlobally').returns(true) + sinon.stub(verify, 'start').resolves() + sinon.stub(spawn, 'start').resolves() }) it('verifies download', function () { diff --git a/cli/test/lib/exec/run_spec.js b/cli/test/lib/exec/run_spec.js index c99c9752c385..4a17bf37f439 100644 --- a/cli/test/lib/exec/run_spec.js +++ b/cli/test/lib/exec/run_spec.js @@ -9,8 +9,8 @@ const verify = require(`${lib}/tasks/verify`) describe('exec run', function () { beforeEach(function () { - this.sandbox.stub(util, 'isInstalledGlobally').returns(true) - this.sandbox.stub(process, 'exit') + sinon.stub(util, 'isInstalledGlobally').returns(true) + sinon.stub(process, 'exit') }) context('.processRunOptions', function () { @@ -39,17 +39,18 @@ describe('exec run', function () { context('.start', function () { beforeEach(function () { - this.sandbox.stub(spawn, 'start').resolves() - this.sandbox.stub(verify, 'start').resolves() + sinon.stub(spawn, 'start').resolves() + sinon.stub(verify, 'start').resolves() }) describe('group and group-id', () => { it('spawns with --group true', function () { return run.start({ group: true, dev: true }) .then(() => { - expect(spawn.start).to.be.calledWith( + expect(spawn.start).to.be.calledWithMatch( ['--run-project', process.cwd(), '--group', true], - { dev: true } + { dev: true, + } ) }) }) diff --git a/cli/test/lib/exec/spawn_spec.js b/cli/test/lib/exec/spawn_spec.js index c4f73a2d00a8..a504edbedebd 100644 --- a/cli/test/lib/exec/spawn_spec.js +++ b/cli/test/lib/exec/spawn_spec.js @@ -5,38 +5,45 @@ const os = require('os') const tty = require('tty') const path = require('path') -const info = require(`${lib}/tasks/info`) +const state = require(`${lib}/tasks/state`) const xvfb = require(`${lib}/exec/xvfb`) const spawn = require(`${lib}/exec/spawn`) const util = require(`${lib}/util.js`) +const expect = require('chai').expect const cwd = process.cwd() -describe('exec spawn', function () { +const defaultBinaryDir = '/default/binary/dir' + +describe('lib/exec/spawn', function () { beforeEach(function () { - this.sandbox.stub(process, 'exit') - this.spawnedProcess = this.sandbox.stub({ - on: () => {}, - unref: () => {}, - stderr: this.sandbox.stub({ - on: () => {}, - }), - }) - this.sandbox.stub(cp, 'spawn').returns(this.spawnedProcess) - this.sandbox.stub(xvfb, 'start').resolves() - this.sandbox.stub(xvfb, 'stop').resolves() - this.sandbox.stub(xvfb, 'isNeeded').returns(false) - this.sandbox.stub(info, 'getPathToExecutable').returns('/path/to/cypress') + os.platform.returns('darwin') + sinon.stub(process, 'exit') + this.spawnedProcess = { + on: sinon.stub().returns(undefined), + unref: sinon.stub().returns(undefined), + stdin: { + on: sinon.stub().returns(undefined), + pipe: sinon.stub().returns(undefined), + }, + stdout: { + on: sinon.stub().returns(undefined), + pipe: sinon.stub().returns(undefined), + }, + stderr: { + pipe: sinon.stub().returns(undefined), + on: sinon.stub().returns(undefined), + }, + } + sinon.stub(cp, 'spawn').returns(this.spawnedProcess) + sinon.stub(xvfb, 'start').resolves() + sinon.stub(xvfb, 'stop').resolves() + sinon.stub(xvfb, 'isNeeded').returns(false) + sinon.stub(state, 'getBinaryDir').returns(defaultBinaryDir) + sinon.stub(state, 'getPathToExecutable').withArgs(defaultBinaryDir).returns('/path/to/cypress') }) context('.start', function () { - afterEach(() => { - delete process.env.FORCE_COLOR - delete process.env.DEBUG_COLORS - delete process.env.MOCHA_COLORS - delete process.env.FORCE_STDERR_TTY - }) - it('passes args + options to spawn', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) @@ -115,6 +122,7 @@ describe('exec spawn', function () { const msg = 'the error message' this.spawnedProcess.on.withArgs('error').yieldsAsync(new Error(msg)) + return spawn.start('--foo') .then(() => { throw new Error('should have hit error handler but did not') @@ -141,95 +149,126 @@ describe('exec spawn', function () { }) }) - it('forces colors when colors are supported', function () { + it('sets process.env to options.env', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - this.sandbox.stub(util, 'supportsColor').returns(true) + process.env.FOO = 'bar' return spawn.start() .then(() => { - 'FORCE_COLOR DEBUG_COLORS MOCHA_COLORS'.split(' ').forEach((prop) => { - expect(process.env[prop], prop).to.eq('1') + expect(cp.spawn.firstCall.args[2].env.FOO).to.eq('bar') + }) + }) + + it('forces colors and streams when supported', function () { + this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + + sinon.stub(util, 'supportsColor').returns(true) + sinon.stub(tty, 'isatty').returns(true) + + return spawn.start([], { env: {} }) + .then(() => { + expect(cp.spawn.firstCall.args[2].env).to.deep.eq({ + FORCE_COLOR: '1', + DEBUG_COLORS: '1', + MOCHA_COLORS: '1', + FORCE_STDERR_TTY: '1', + FORCE_STDIN_TTY: '1', + FORCE_STDOUT_TTY: '1', }) }) }) - it('does not force colors when colors are not supported', function () { + it('does not force colors and streams when not supported', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - this.sandbox.stub(util, 'supportsColor').returns(false) + sinon.stub(util, 'supportsColor').returns(false) + sinon.stub(tty, 'isatty').returns(false) - return spawn.start() + return spawn.start([], { env: {} }) .then(() => { - 'FORCE_COLOR DEBUG_COLORS MOCHA_COLORS'.split(' ').forEach((prop) => { - expect(process.env[prop], prop).to.be.undefined + expect(cp.spawn.firstCall.args[2].env).to.deep.eq({ + FORCE_COLOR: '0', + DEBUG_COLORS: '0', + FORCE_STDERR_TTY: '0', + FORCE_STDIN_TTY: '0', + FORCE_STDOUT_TTY: '0', }) }) }) - it('forces stderr tty when needs xvfb and stderr is tty', function () { + it('pipes when on win32', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + os.platform.returns('win32') + xvfb.isNeeded.returns(false) - this.sandbox.stub(tty, 'isatty').returns(true) - this.sandbox.stub(os, 'platform').returns('linux') - xvfb.isNeeded.returns(true) + return spawn.start() + .then(() => { + expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq('pipe') + }) + }) + + it('inherits when on linux and xvfb isnt needed', function () { + this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + os.platform.returns('linux') + xvfb.isNeeded.returns(false) return spawn.start() .then(() => { - expect(process.env.FORCE_STDERR_TTY, 'FORCE_STDERR_TTY').to.eq('1') + expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq('inherit') }) }) - it('does not force stderr tty when needs xvfb isnt needed', function () { + it('uses [inherit, inherit, pipe] when linux and xvfb is needed', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - this.sandbox.stub(tty, 'isatty').returns(true) - this.sandbox.stub(os, 'platform').returns('linux') + xvfb.isNeeded.returns(true) + os.platform.returns('linux') return spawn.start() .then(() => { - expect(process.env.FORCE_STDERR_TTY).to.be.undefined + expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq([ + 'inherit', 'inherit', 'pipe', + ]) }) }) - it('does not force stderr tty when stderr is not currently tty', function () { + it('uses [inherit, inherit, pipe] on darwin', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - this.sandbox.stub(tty, 'isatty').returns(false) - this.sandbox.stub(os, 'platform').returns('linux') - xvfb.isNeeded.returns(true) + xvfb.isNeeded.returns(false) + os.platform.returns('darwin') return spawn.start() .then(() => { - expect(process.env.FORCE_STDERR_TTY).to.be.undefined + expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq([ + 'inherit', 'inherit', 'pipe', + ]) }) }) - it('writes to process.stderr when piping', function () { + it('writes everything on win32', function () { const buf1 = new Buffer('asdf') + this.spawnedProcess.stdin.pipe.withArgs(process.stdin) + this.spawnedProcess.stdout.pipe.withArgs(process.stdout) + this.spawnedProcess.stderr.on .withArgs('data') .yields(buf1) - xvfb.isNeeded.returns(true) - this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - this.sandbox.stub(process.stderr, 'write') - this.sandbox.stub(tty, 'isatty').returns(false) - this.sandbox.stub(os, 'platform').returns('linux') - xvfb.isNeeded.returns(true) + sinon.stub(process.stderr, 'write').withArgs(buf1) + os.platform.returns('win32') return spawn.start() - .then(() => { - expect(process.stderr.write).to.be.calledWith(buf1) - }) }) it('does not write to process.stderr when from xlib or libudev', function () { const buf1 = new Buffer('Xlib: something foo') const buf2 = new Buffer('libudev something bar') + const buf3 = new Buffer('asdf') this.spawnedProcess.stderr.on .withArgs('data') @@ -237,14 +276,13 @@ describe('exec spawn', function () { .yields(buf1) .onSecondCall() .yields(buf2) - - xvfb.isNeeded.returns(true) + .onThirdCall() + .yields(buf3) this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - this.sandbox.stub(process.stderr, 'write') - this.sandbox.stub(tty, 'isatty').returns(false) - this.sandbox.stub(os, 'platform').returns('linux') + sinon.stub(process.stderr, 'write').withArgs(buf3) + os.platform.returns('linux') xvfb.isNeeded.returns(true) return spawn.start() @@ -254,37 +292,25 @@ describe('exec spawn', function () { }) }) - it('filters out process.stderr when piping') - - it('uses inherit/inherit/pipe when linux and xvfb is needed', function () { - xvfb.isNeeded.returns(true) + it('does not write to process.stderr when from high sierra warnings', function () { + const buf1 = new Buffer('2018-05-19 15:30:30.287 Cypress[7850:32145] *** WARNING: Textured Window') + const buf2 = new Buffer('asdf') - this.sandbox.stub(os, 'platform').returns('linux') + this.spawnedProcess.stderr.on + .withArgs('data') + .onFirstCall() + .yields(buf1) + .onSecondCall(buf2) + .yields(buf2) this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + sinon.stub(process.stderr, 'write').withArgs(buf2) + os.platform.returns('darwin') + return spawn.start() .then(() => { - expect(cp.spawn.firstCall.args[2]).to.deep.eq({ - detached: false, - stdio: ['inherit', 'inherit', 'pipe'], - }) - }) - }) - - ;['win32', 'darwin', 'linux'].forEach((platform) => { - it(`uses inherit when '${platform}' and xvfb is not needed`, function () { - this.sandbox.stub(os, 'platform').returns(platform) - - this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - - return spawn.start() - .then(() => { - expect(cp.spawn.firstCall.args[2]).to.deep.eq({ - detached: false, - stdio: 'inherit', - }) - }) + expect(process.stderr.write).not.to.be.calledWith(buf1) }) }) }) diff --git a/cli/test/lib/exec/xvfb_spec.js b/cli/test/lib/exec/xvfb_spec.js index e191c4c6064d..b25014ba95ca 100644 --- a/cli/test/lib/exec/xvfb_spec.js +++ b/cli/test/lib/exec/xvfb_spec.js @@ -3,11 +3,15 @@ require('../../spec_helper') const os = require('os') const xvfb = require(`${lib}/exec/xvfb`) -describe('exec xvfb', function () { +describe('lib/exec/xvfb', function () { + beforeEach(function () { + os.platform.returns('win32') + }) + context('debugXvfb', function () { it('outputs when enabled', function () { - this.sandbox.stub(process.stderr, 'write') - this.sandbox.stub(xvfb._debugXvfb, 'enabled').value(true) + sinon.stub(process.stderr, 'write').returns(undefined) + sinon.stub(xvfb._debugXvfb, 'enabled').value(true) xvfb._xvfb._onStderrData('asdf') @@ -16,8 +20,8 @@ describe('exec xvfb', function () { }) it('does not output when disabled', function () { - this.sandbox.stub(process.stderr, 'write') - this.sandbox.stub(xvfb._debugXvfb, 'enabled').value(false) + sinon.stub(process.stderr, 'write') + sinon.stub(xvfb._debugXvfb, 'enabled').value(false) xvfb._xvfb._onStderrData('asdf') @@ -28,13 +32,14 @@ describe('exec xvfb', function () { context('#start', function () { it('passes', function () { - this.sandbox.stub(xvfb._xvfb, 'startAsync').resolves() + sinon.stub(xvfb._xvfb, 'startAsync').resolves() return xvfb.start() }) it('fails with error message', function () { const message = 'nope' - this.sandbox.stub(xvfb._xvfb, 'startAsync').rejects(new Error(message)) + sinon.stub(xvfb._xvfb, 'startAsync').rejects(new Error(message)) + return xvfb.start() .then(() => { throw new Error('Should have thrown an error') @@ -48,7 +53,7 @@ describe('exec xvfb', function () { const e = new Error('something bad happened') e.nonZeroExitCode = true - this.sandbox.stub(xvfb._xvfb, 'startAsync').rejects(e) + sinon.stub(xvfb._xvfb, 'startAsync').rejects(e) return xvfb.start() .then(() => { @@ -63,16 +68,15 @@ describe('exec xvfb', function () { }) context('#isNeeded', function () { - afterEach(() => delete process.env.DISPLAY) it('does not need xvfb on osx', function () { - this.sandbox.stub(os, 'platform').returns('darwin') + os.platform.returns('darwin') expect(xvfb.isNeeded()).to.be.false }) it('does not need xvfb on linux when DISPLAY is set', function () { - this.sandbox.stub(os, 'platform').returns('linux') + os.platform.returns('linux') process.env.DISPLAY = ':99' @@ -80,7 +84,7 @@ describe('exec xvfb', function () { }) it('does need xvfb on linux when no DISPLAY is set', function () { - this.sandbox.stub(os, 'platform').returns('linux') + os.platform.returns('linux') expect(xvfb.isNeeded()).to.be.true }) diff --git a/cli/test/lib/tasks/download_spec.js b/cli/test/lib/tasks/download_spec.js index 8ce2b87f6372..4c1d576f1f5b 100644 --- a/cli/test/lib/tasks/download_spec.js +++ b/cli/test/lib/tasks/download_spec.js @@ -1,21 +1,24 @@ require('../../spec_helper') const os = require('os') -const nock = require('nock') -const snapshot = require('snap-shot-it') const la = require('lazy-ass') const is = require('check-more-types') +const path = require('path') +const nock = require('nock') +const snapshot = require('snap-shot-it') const fs = require(`${lib}/fs`) const logger = require(`${lib}/logger`) const util = require(`${lib}/util`) -const info = require(`${lib}/tasks/info`) const download = require(`${lib}/tasks/download`) const stdout = require('../../support/stdout') const normalize = require('../../support/normalize') -describe('download', function () { +const downloadDestination = path.join(os.tmpdir(), 'Cypress', 'download', 'cypress.zip') +const version = '1.2.3' + +describe('lib/tasks/download', function () { require('mocha-banner').register() const rootFolder = '/home/user/git' @@ -25,12 +28,14 @@ describe('download', function () { this.stdout = stdout.capture() - this.options = { displayOpen: false } + this.options = { + downloadDestination, + version, + } - this.sandbox.stub(os, 'platform').returns('darwin') - this.sandbox.stub(os, 'release').returns('test release') - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') - this.sandbox.stub(util, 'cwd').returns(rootFolder) + os.platform.returns('darwin') + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(util, 'cwd').returns(rootFolder) }) afterEach(function () { @@ -66,21 +71,48 @@ describe('download', function () { }) }) - it('sets options.version to response x-version', function () { + it('saves example.zip to options.downloadDestination', function () { + nock('https://aws.amazon.com') + .get('/some.zip') + .reply(200, () => fs.createReadStream('test/fixture/example.zip')) + + nock('https://download.cypress.io') + .get('/desktop/1.2.3') + .query(true) + .reply(302, undefined, { + Location: 'https://aws.amazon.com/some.zip', + 'x-version': '0.11.1', + }) + + + const onProgress = sinon.stub().returns(undefined) + + return download.start({ + downloadDestination: this.options.downloadDestination, + version: this.options.version, + progress: { onProgress }, + }) + .then((responseVersion) => { + expect(responseVersion).to.eq('0.11.1') + return fs.statAsync(downloadDestination) + }) + }) + + it('resolves with response x-version if present', function () { nock('https://aws.amazon.com') .get('/some.zip') .reply(200, () => fs.createReadStream('test/fixture/example.zip')) nock('https://download.cypress.io') - .get('/desktop') + .get('/desktop/1.2.3') .query(true) .reply(302, undefined, { Location: 'https://aws.amazon.com/some.zip', 'x-version': '0.11.1', }) - return download.start(this.options).then(() => { - expect(this.options.version).to.eq('0.11.1') + return download.start(this.options).then((responseVersion) => { + expect(responseVersion).to.eq('0.11.1') }) }) @@ -99,8 +131,9 @@ describe('download', function () { 'x-version': '0.13.0', }) - return download.start(this.options).then(() => { - expect(this.options.version).to.eq('0.13.0') + return download.start(this.options).then((responseVersion) => { + expect(responseVersion).to.eq('0.13.0') + return fs.statAsync(downloadDestination) }) }) @@ -110,10 +143,11 @@ describe('download', function () { const err = new Error() err.statusCode = 404 err.statusMessage = 'Not Found' + this.options.version = null // not really the download error, but the easiest way to // test the error handling - this.sandbox.stub(info, 'ensureInstallationDir').rejects(err) + sinon.stub(fs, 'ensureDirAsync').rejects(err) return download .start(this.options) @@ -123,7 +157,7 @@ describe('download', function () { .catch((err) => { logger.error(err) - snapshot('download status errors', normalize(ctx.stdout.toString())) + return snapshot('download status errors', normalize(ctx.stdout.toString())) }) }) }) diff --git a/cli/test/lib/tasks/info_spec.js b/cli/test/lib/tasks/info_spec.js deleted file mode 100644 index a1962f546e98..000000000000 --- a/cli/test/lib/tasks/info_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -require('../../spec_helper') - -const os = require('os') -const path = require('path') - -const fs = require(`${lib}/fs`) -const logger = require(`${lib}/logger`) -const info = require(`${lib}/tasks/info`) - -const installationDir = info.getInstallationDir() -const infoFilePath = info.getInfoFilePath() - -describe('info', function () { - beforeEach(function () { - logger.reset() - - this.sandbox.stub(process, 'exit') - this.ensureEmptyInstallationDir = () => { - return fs.removeAsync(installationDir) - .then(() => { - return info.ensureInstallationDir() - }) - } - }) - - afterEach(function () { - return fs.removeAsync(installationDir) - }) - - context('.clearVersionState', function () { - it('wipes out version info in info.json', function () { - return fs.outputJsonAsync(infoFilePath, { version: '5', verifiedVersion: '5', other: 'info' }) - .then(() => { - return info.clearVersionState() - }) - .then(() => { - return fs.readJsonAsync(infoFilePath) - }) - .then((contents) => { - expect(contents).to.eql({ other: 'info' }) - }) - }) - }) - - context('.ensureInstallationDir', function () { - beforeEach(function () { - return fs.removeAsync(installationDir) - }) - - it('ensures directory exists', function () { - return info.ensureInstallationDir().then(() => { - return fs.statAsync(installationDir) - }) - }) - }) - - context('.getInstallationDir', function () { - it('resolves path to installation directory', function () { - expect(info.getInstallationDir()).to.equal(installationDir) - }) - }) - - context('.getInstalledVersion', function () { - beforeEach(function () { - return this.ensureEmptyInstallationDir() - }) - - it('resolves version from version file when it exists', function () { - return info.writeInstalledVersion('2.0.48') - .then(() => { - return info.getInstalledVersion() - }) - .then((version) => { - expect(version).to.equal('2.0.48') - }) - }) - - it('throws when version file does not exist', function () { - return info.getInstalledVersion() - .catch(() => {}) - }) - }) - - context('.getPathToExecutable', function () { - it('resolves path on windows', function () { - this.sandbox.stub(os, 'platform').returns('win32') - expect(info.getPathToExecutable()).to.endWith('.exe') - }) - }) - - context('.getPathToUserExecutableDir', function () { - it('resolves path on macOS', function () { - this.sandbox.stub(os, 'platform').returns('darwin') - expect(info.getPathToUserExecutableDir()).to.equal(path.join(installationDir, 'Cypress.app')) - }) - - it('resolves path on linux', function () { - this.sandbox.stub(os, 'platform').returns('linux') - expect(info.getPathToUserExecutableDir()).to.equal(path.join(installationDir, 'Cypress')) - }) - - it('resolves path on windows', function () { - this.sandbox.stub(os, 'platform').returns('win32') - expect(info.getPathToUserExecutableDir()).to.endWith('Cypress') - }) - - it('rejects on anything else', function () { - this.sandbox.stub(os, 'platform').returns('unknown') - expect(() => info.getPathToUserExecutableDir()).to.throw('Platform: "unknown" is not supported.') - }) - }) - - context('.writeInstalledVersion', function () { - beforeEach(function () { - return this.ensureEmptyInstallationDir() - }) - - it('writes the version to the version file', function () { - return info.writeInstalledVersion('the version') - .then(() => { - return fs.readJsonAsync(infoFilePath).get('version') - }) - .then((version) => { - expect(version).to.equal('the version') - }) - }) - }) -}) diff --git a/cli/test/lib/tasks/install_spec.js b/cli/test/lib/tasks/install_spec.js index 38fa733567a0..78f96c80fdd1 100644 --- a/cli/test/lib/tasks/install_spec.js +++ b/cli/test/lib/tasks/install_spec.js @@ -1,5 +1,6 @@ require('../../spec_helper') - +const os = require('os') +const path = require('path') const chalk = require('chalk') const Promise = require('bluebird') const snapshot = require('snap-shot-it') @@ -9,7 +10,7 @@ const stdout = require('../../support/stdout') const fs = require(`${lib}/fs`) const download = require(`${lib}/tasks/download`) const install = require(`${lib}/tasks/install`) -const info = require(`${lib}/tasks/info`) +const state = require(`${lib}/tasks/state`) const unzip = require(`${lib}/tasks/unzip`) const logger = require(`${lib}/logger`) const util = require(`${lib}/util`) @@ -17,12 +18,10 @@ const util = require(`${lib}/util`) const normalize = require('../../support/normalize') const packageVersion = '1.2.3' -const downloadDestination = { - filename: 'path/to/cypress.zip', - downloaded: true, -} +const downloadDestination = path.join(os.tmpdir(), 'cypress.zip') +const installDir = '/cache/Cypress/1.2.3' -describe('install', function () { +describe('/lib/tasks/install', function () { require('mocha-banner').register() beforeEach(function () { @@ -43,25 +42,24 @@ describe('install', function () { beforeEach(function () { logger.reset() - this.sandbox.stub(util, 'isCi').returns(false) - this.sandbox.stub(util, 'pkgVersion').returns(packageVersion) - this.sandbox.stub(download, 'start').resolves(downloadDestination) - this.sandbox.stub(unzip, 'start').resolves() - this.sandbox.stub(Promise, 'delay').resolves() - this.sandbox.stub(fs, 'removeAsync').resolves() - this.sandbox.stub(info, 'getPathToUserExecutableDir').returns('/path/to/binary/dir/') - this.sandbox.stub(info, 'getInstalledVersion').resolves() - this.sandbox.stub(info, 'writeInstalledVersion').resolves() - this.sandbox.stub(info, 'clearVersionState').resolves() + // sinon.stub(os, 'tmpdir').returns('/tmp') + sinon.stub(util, 'isCi').returns(false) + sinon.stub(util, 'pkgVersion').returns(packageVersion) + sinon.stub(download, 'start').resolves(packageVersion) + sinon.stub(unzip, 'start').resolves() + sinon.stub(Promise, 'delay').resolves() + sinon.stub(fs, 'removeAsync').resolves() + sinon.stub(state, 'getVersionDir').returns('/cache/Cypress/1.2.3') + sinon.stub(state, 'getBinaryDir').returns('/cache/Cypress/1.2.3/Cypress.app') + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves() + sinon.stub(fs, 'ensureDirAsync').resolves(undefined) + os.platform.returns('darwin') }) describe('skips install', function () { - afterEach(function () { - delete process.env.CYPRESS_SKIP_BINARY_INSTALL - }) it('when environment variable is set', function () { - process.env.CYPRESS_SKIP_BINARY_INSTALL = true + process.env.CYPRESS_INSTALL_BINARY = '0' return install.start() .then(() => { @@ -76,13 +74,10 @@ describe('install', function () { }) describe('override version', function () { - afterEach(function () { - delete process.env.CYPRESS_BINARY_VERSION - }) it('warns when specifying cypress version in env', function () { const version = '0.12.1' - process.env.CYPRESS_BINARY_VERSION = version + process.env.CYPRESS_INSTALL_BINARY = version return install.start() .then(() => { @@ -91,7 +86,7 @@ describe('install', function () { }) expect(unzip.start).to.be.calledWithMatch({ - version, + zipFilePath: downloadDestination, }) snapshot( @@ -103,178 +98,262 @@ describe('install', function () { it('can install local binary zip file without download', function () { const version = '/tmp/local/file.zip' - process.env.CYPRESS_BINARY_VERSION = version - this.sandbox.stub(fs, 'statAsync').withArgs(version).resolves() + process.env.CYPRESS_INSTALL_BINARY = version + sinon.stub(fs, 'pathExistsAsync').withArgs(version).resolves(true) + const installDir = state.getVersionDir() return install.start() .then(() => { - expect(unzip.start).calledWith({ - zipDestination: version, - destination: info.getInstallationDir(), - executable: info.getPathToUserExecutableDir(), + expect(unzip.start).to.be.calledWithMatch({ + zipFilePath: version, + installDir, }) - expect(info.writeInstalledVersion).calledWith('unknown') }) }) - }) - describe('when version is already installed', function () { - beforeEach(function () { - info.getInstalledVersion.resolves(packageVersion) + describe('when version is already installed', function () { + beforeEach(function () { + state.getBinaryPkgVersionAsync.resolves(packageVersion) - return install.start() - }) + return install.start() + }) - it('logs noop message', function () { - expect(download.start).not.to.be.called + it('logs noop message', function () { + expect(download.start).not.to.be.called - snapshot( - 'version already installed', - normalize(this.stdout.toString()) - ) + snapshot( + 'version already installed', + normalize(this.stdout.toString()) + ) + }) }) - }) - describe('when getting installed version fails', function () { - beforeEach(function () { - info.getInstalledVersion.rejects(new Error('no')) + describe('when getting installed version fails', function () { + beforeEach(function () { + state.getBinaryPkgVersionAsync.resolves(null) - return install.start() - }) + return install.start() + }) + + it('logs message and starts download', function () { + expect(download.start).to.be.calledWithMatch({ + version: packageVersion, + }) - it('logs message and starts download', function () { - expect(download.start).to.be.calledWithMatch({ - version: packageVersion, + expect(unzip.start).to.be.calledWithMatch({ + installDir, + }) + + snapshot( + 'continues installing on failure', + normalize(this.stdout.toString()) + ) }) + }) + + describe('when there is no install version', function () { + beforeEach(function () { + state.getBinaryPkgVersionAsync.resolves(null) - expect(unzip.start).to.be.calledWithMatch({ - version: packageVersion, + return install.start() }) - snapshot( - 'continues installing on failure', - normalize(this.stdout.toString()) - ) - }) - }) + it('logs message and starts download', function () { - describe('when there is no install version', function () { - beforeEach(function () { - info.getInstalledVersion.resolves(null) + expect(download.start).to.be.calledWithMatch({ + version: packageVersion, + }) - return install.start() - }) + expect(unzip.start).to.be.calledWithMatch({ + installDir, + }) - it('logs message and starts download', function () { - expect(info.clearVersionState).to.be.called + // cleans up the zip file + expect(fs.removeAsync).to.be.calledWith( + downloadDestination + ) - expect(download.start).to.be.calledWithMatch({ - version: packageVersion, + snapshot( + 'installs without existing installation', + normalize(this.stdout.toString()) + ) }) + }) - expect(unzip.start).to.be.calledWithMatch({ - version: packageVersion, + describe('when getting installed version does not match needed version', function () { + beforeEach(function () { + state.getBinaryPkgVersionAsync.resolves('x.x.x') + + return install.start() }) - // cleans up the zip file - expect(fs.removeAsync).to.be.calledWith( - downloadDestination.filename - ) + it('logs message and starts download', function () { + expect(download.start).to.be.calledWithMatch({ + version: packageVersion, + }) - snapshot( - 'installs without existing installation', - normalize(this.stdout.toString()) - ) + expect(unzip.start).to.be.calledWithMatch({ + installDir, + }) + + snapshot( + 'installed version does not match needed version', + normalize(this.stdout.toString()) + ) + }) }) - }) - describe('when getting installed version does not match needed version', function () { - beforeEach(function () { - info.getInstalledVersion.resolves('x.x.x') + describe('with force: true', function () { + beforeEach(function () { + state.getBinaryPkgVersionAsync.resolves(packageVersion) - return install.start() - }) + return install.start({ force: true }) + }) + + it('logs message and starts download', function () { + + expect(download.start).to.be.calledWithMatch({ + version: packageVersion, + }) - it('logs message and starts download', function () { - expect(download.start).to.be.calledWithMatch({ - version: packageVersion, + expect(unzip.start).to.be.calledWithMatch({ + installDir, + }) + + snapshot( + 'forcing true always installs', + normalize(this.stdout.toString()) + ) }) + }) + + describe('as a global install', function () { + beforeEach(function () { + sinon.stub(util, 'isInstalledGlobally').returns(true) - expect(unzip.start).to.be.calledWithMatch({ - version: packageVersion, + state.getBinaryPkgVersionAsync.resolves('x.x.x') + + return install.start() }) - snapshot( - 'installed version does not match needed version', - normalize(this.stdout.toString()) - ) - }) - }) + it('logs global warning and download', function () { + expect(download.start).to.be.calledWithMatch({ + version: packageVersion, + }) - describe('with force: true', function () { - beforeEach(function () { - info.getInstalledVersion.resolves(packageVersion) + expect(unzip.start).to.be.calledWithMatch({ + installDir, + }) - return install.start({ force: true }) + snapshot( + 'warning installing as global', + normalize(this.stdout.toString()) + ) + }) }) - it('logs message and starts download', function () { - expect(info.clearVersionState).to.be.called + describe('when running in CI', function () { + beforeEach(function () { + util.isCi.returns(true) - expect(download.start).to.be.calledWithMatch({ - version: packageVersion, - }) + state.getBinaryPkgVersionAsync.resolves('x.x.x') - expect(unzip.start).to.be.calledWithMatch({ - version: packageVersion, + return install.start() }) - snapshot( - 'forcing true always installs', - normalize(this.stdout.toString()) - ) + it('uses verbose renderer', function () { + snapshot( + 'installing in ci', + normalize(this.stdout.toString()) + ) + }) }) - }) - describe('as a global install', function () { - beforeEach(function () { - this.sandbox.stub(util, 'isInstalledGlobally').returns(true) + describe('failed write access to cache directory', function () { + it('logs error on failure', function () { + os.platform.returns('darwin') + sinon.stub(state, 'getCacheDir').returns('/invalid/cache/dir') - info.getInstalledVersion.resolves('x.x.x') + const err = new Error('EACCES: permission denied, mkdir \'/invalid\'') + err.code = 'EACCES' + fs.ensureDirAsync.rejects(err) - return install.start() - }) + return install.start() + .then(() => { + throw new Error('should have caught error') + }) + .catch((err) => { + logger.error(err) - it('logs global warning and download', function () { - expect(download.start).to.be.calledWithMatch({ - version: packageVersion, + snapshot( + 'invalid cache directory', + normalize(this.stdout.toString()) + ) + }) }) + }) - expect(unzip.start).to.be.calledWithMatch({ - version: packageVersion, + describe('CYPRESS_INSTALL_BINARY is URL or Zip', function () { + it('uses cache when correct version installed given URL', function () { + state.getBinaryPkgVersionAsync.resolves('1.2.3') + util.pkgVersion.returns('1.2.3') + process.env.CYPRESS_INSTALL_BINARY = 'www.cypress.io/cannot-download/2.4.5' + return install.start() + .then(() => { + expect(download.start).to.not.be.called + }) + }) + it('uses cache when mismatch version given URL ', function () { + state.getBinaryPkgVersionAsync.resolves('1.2.3') + util.pkgVersion.returns('4.0.0') + process.env.CYPRESS_INSTALL_BINARY = 'www.cypress.io/cannot-download/2.4.5' + return install.start() + .then(() => { + expect(download.start).to.not.be.called + }) }) + it('uses cache when correct version installed given Zip', function () { + sinon.stub(fs, 'pathExistsAsync').withArgs('/path/to/zip.zip').resolves(true) - snapshot( - 'warning installing as global', - normalize(this.stdout.toString()) - ) - }) - }) + state.getBinaryPkgVersionAsync.resolves('1.2.3') + util.pkgVersion.returns('1.2.3') - describe('when running in CI', function () { - beforeEach(function () { - util.isCi.returns(true) + process.env.CYPRESS_INSTALL_BINARY = '/path/to/zip.zip' + return install.start() + .then(() => { + expect(unzip.start).to.not.be.called + }) + }) + it('uses cache when mismatch version given Zip ', function () { + sinon.stub(fs, 'pathExistsAsync').withArgs('/path/to/zip.zip').resolves(true) + + state.getBinaryPkgVersionAsync.resolves('1.2.3') + util.pkgVersion.returns('4.0.0') + process.env.CYPRESS_INSTALL_BINARY = '/path/to/zip.zip' + return install.start() + .then(() => { + expect(unzip.start).to.not.be.called + }) + }) + }) - info.getInstalledVersion.resolves('x.x.x') + describe('CYPRESS_BINARY_VERSION', function () { + it('throws when env var CYPRESS_BINARY_VERSION', function () { + process.env.CYPRESS_BINARY_VERSION = '/asf/asf' - return install.start() - }) + return install.start() + .then(() => { + throw new Error('should have thrown') + }) + .catch((err) => { + logger.error(err) - it('uses verbose renderer', function () { - snapshot( - 'installing in ci', - normalize(this.stdout.toString()) - ) + snapshot( + 'error for removed CYPRESS_BINARY_VERSION', + normalize(this.stdout.toString()) + ) + }) + }) }) }) }) diff --git a/cli/test/lib/tasks/state_spec.js b/cli/test/lib/tasks/state_spec.js new file mode 100644 index 000000000000..d22b82c92bb1 --- /dev/null +++ b/cli/test/lib/tasks/state_spec.js @@ -0,0 +1,187 @@ +require('../../spec_helper') + +const os = require('os') +const path = require('path') +const proxyquire = require('proxyquire') +// const Promise = require('bluebird') + +const fs = require(`${lib}/fs`) +const logger = require(`${lib}/logger`) +const util = require(`${lib}/util`) + +const fakeCachedir = (cacheName) => path.join('.cache', cacheName) + +const cacheDir = path.join(fakeCachedir('Cypress')) +const versionDir = path.join(cacheDir, '1.2.3') +const binaryDir = path.join(versionDir, 'Cypress.app') +const binaryPkgPath = path.join(binaryDir, 'Contents', 'Resources', 'app', 'package.json') + +let state + +describe('lib/tasks/state', function () { + beforeEach(function () { + state = proxyquire(`${lib}/tasks/state`, { cachedir: fakeCachedir }) + logger.reset() + sinon.stub(process, 'exit') + sinon.stub(util, 'pkgVersion').returns('1.2.3') + os.platform.returns('darwin') + }) + + context('.getBinaryPkgVersionAsync', function () { + + it('resolves version from version file when it exists', function () { + sinon.stub(fs, 'pathExistsAsync').withArgs(binaryPkgPath).resolves(true) + sinon.stub(fs, 'readJsonAsync').withArgs(binaryPkgPath).resolves({ version: '2.0.48' }) + return state.getBinaryPkgVersionAsync(binaryDir) + .then((binaryVersion) => { + expect(binaryVersion).to.equal('2.0.48') + }) + }) + + it('returns null if no version found', function () { + sinon.stub(fs, 'pathExistsAsync').resolves(false) + return state.getBinaryPkgVersionAsync(binaryDir) + .then((binaryVersion) => expect(binaryVersion).to.equal(null)) + }) + + it('returns correct version if passed binaryDir', function () { + const customBinaryDir = '/custom/binary/dir' + const customBinaryPackageDir = '/custom/binary/dir/Contents/Resources/app/package.json' + sinon.stub(fs, 'pathExistsAsync').withArgs(customBinaryPackageDir).resolves(true) + sinon.stub(fs, 'readJsonAsync').withArgs(customBinaryPackageDir).resolves({ version: '3.4.5' }) + + return state.getBinaryPkgVersionAsync(customBinaryDir) + .then((binaryVersion) => expect(binaryVersion).to.equal('3.4.5')) + }) + + }) + + context('.getPathToExecutable', function () { + it('resolves path on macOS', function () { + const macExecutable = '.cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress' + expect(state.getPathToExecutable(state.getBinaryDir())).to.equal(macExecutable) + }) + it('resolves path on linux', function () { + os.platform.returns('linux') + const linuxExecutable = '.cache/Cypress/1.2.3/Cypress/Cypress' + expect(state.getPathToExecutable(state.getBinaryDir())).to.equal(linuxExecutable) + }) + it('resolves path on windows', function () { + os.platform.returns('win32') + expect(state.getPathToExecutable(state.getBinaryDir())).to.endWith('.exe') + }) + it('resolves from custom binaryDir', function () { + const customBinaryDir = 'home/downloads/cypress.app' + expect(state.getPathToExecutable(customBinaryDir)).to.equal('home/downloads/cypress.app/Contents/MacOS/Cypress') + }) + }) + + context('.getBinaryDir', function () { + it('resolves path on macOS', function () { + expect(state.getBinaryDir()).to.equal(path.join(versionDir, 'Cypress.app')) + }) + + it('resolves path on linux', function () { + os.platform.returns('linux') + expect(state.getBinaryDir()).to.equal(path.join(versionDir, 'Cypress')) + }) + + it('resolves path on windows', function () { + const state = proxyquire(`${lib}/tasks/state`, { path: path.win32, cachedir: fakeCachedir }) + os.platform.returns('win32') + const pathToExec = state.getBinaryDir() + expect(pathToExec).to.be.equal(path.win32.join(versionDir, 'Cypress')) + }) + + it('resolves path to binary/installation directory', function () { + expect(state.getBinaryDir()).to.equal(binaryDir) + }) + + it('resolves path to binary/installation from version', function () { + expect(state.getBinaryDir('4.5.6')).to.be.equal(path.join(cacheDir, '4.5.6', 'Cypress.app')) + }) + + it('rejects on anything else', function () { + os.platform.returns('unknown') + expect(() => state.getBinaryDir().to.throw('Platform: "unknown" is not supported.') + ) + }) + }) + + context('.getBinaryVerifiedAsync', function () { + it('resolves true if verified', function () { + sinon.stub(fs, 'readJsonAsync').resolves({ verified: true }) + return state.getBinaryVerifiedAsync('/asdf') + .then((isVerified) => expect(isVerified).to.be.equal(true)) + }) + it('resolves undefined if not verified', function () { + sinon.stub(fs, 'readJsonAsync').rejects({ code: 'ENOENT' }) + return state.getBinaryVerifiedAsync('/asdf') + .then((isVerified) => expect(isVerified).to.be.equal(undefined)) + }) + it('can accept custom binaryDir', function () { + const customBinaryDir = '/custom/binary/dir' + sinon.stub(fs, 'pathExistsAsync').withArgs('/custom/binary/dir/binary_state.json').resolves({ verified: true }) + sinon.stub(fs, 'readJsonAsync').withArgs('/custom/binary/dir/binary_state.json').resolves({ verified: true }) + return state.getBinaryVerifiedAsync(customBinaryDir) + .then((isVerified) => expect(isVerified).to.be.equal(true)) + }) + }) + context('.writeBinaryVerified', function () { + it('writes to binary state verified:true', function () { + sinon.stub(fs, 'outputJsonAsync').resolves() + return state.writeBinaryVerifiedAsync(true, binaryDir) + .then(() => expect(fs.outputJsonAsync).to.be.calledWith( + path.join(binaryDir, 'binary_state.json'), { verified: true }), { spaces: 2 } + ) + }) + + it('write to binary state verified:false', function () { + sinon.stub(fs, 'outputJsonAsync').resolves() + return state.writeBinaryVerifiedAsync(false, binaryDir) + .then(() => expect(fs.outputJsonAsync).to.be.calledWith( + path.join(binaryDir, 'binary_state.json'), { verified: false }, { spaces: 2 }) + ) + }) + }) + context('.getCacheDir', function () { + + it('uses cachedir()', function () { + const ret = state.getCacheDir() + expect(ret).to.equal(cacheDir) + }) + + it('uses env variable CYPRESS_CACHE_FOLDER', function () { + process.env.CYPRESS_CACHE_FOLDER = '/path/to/dir' + const ret = state.getCacheDir() + expect(ret).to.equal('/path/to/dir') + }) + }) + context('.parsePlatformBinaryFolder', function () { + + it('can parse on darwin', function () { + os.platform.returns('darwin') + expect(state.parsePlatformBinaryFolder('/Documents/Cypress.app/Contents/MacOS/Cypress')).to.equal('/Documents/Cypress.app') + }) + it('can parse on linux', function () { + os.platform.returns('linux') + expect(state.parsePlatformBinaryFolder('/Documents/Cypress/Cypress')).to.equal('/Documents/Cypress') + }) + it('can parse on darwin', function () { + os.platform.returns('win32') + expect(state.parsePlatformBinaryFolder('/Documents/Cypress/Cypress.exe')).to.equal('/Documents/Cypress') + }) + it('throws when invalid on darwin', function () { + os.platform.returns('darwin') + expect(state.parsePlatformBinaryFolder('/Documents/Cypress.app')).to.equal(false) + }) + it('throws when invalid on linux', function () { + os.platform.returns('linux') + expect(state.parsePlatformBinaryFolder('/Documents/Cypress.app')).to.equal(false) + }) + it('throws when invalid on windows', function () { + os.platform.returns('win32') + expect(state.parsePlatformBinaryFolder('/Documents/Cypress.app')).to.equal(false) + }) + }) +}) diff --git a/cli/test/lib/tasks/unzip_spec.js b/cli/test/lib/tasks/unzip_spec.js index 1f8fcbe8e80c..58f53d37ff5e 100644 --- a/cli/test/lib/tasks/unzip_spec.js +++ b/cli/test/lib/tasks/unzip_spec.js @@ -7,28 +7,28 @@ const snapshot = require('snap-shot-it') const fs = require(`${lib}/fs`) const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) -const info = require(`${lib}/tasks/info`) const unzip = require(`${lib}/tasks/unzip`) const stdout = require('../../support/stdout') const normalize = require('../../support/normalize') -const dest = info.getInstallationDir() +const version = '1.2.3' +const installDir = path.join(os.tmpdir(), 'Cypress', version) -describe('unzip', function () { + +describe('lib/tasks/unzip', function () { require('mocha-banner').register() beforeEach(function () { this.stdout = stdout.capture() - this.sandbox.stub(os, 'platform').returns('darwin') - this.sandbox.stub(os, 'release').returns('test release') - this.sandbox.stub(util, 'pkgVersion').returns('1.2.3') + os.platform.returns('darwin') + sinon.stub(util, 'pkgVersion').returns(version) }) afterEach(function () { stdout.restore() - return fs.removeAsync(dest) + // return fs.removeAsync(installationDir) }) it('throws when cannot unzip', function () { @@ -36,8 +36,8 @@ describe('unzip', function () { return unzip .start({ - downloadDestination: path.join('test', 'fixture', 'bad_example.zip'), - zipDestination: '/foo/bar/baz', + zipFilePath: path.join('test', 'fixture', 'bad_example.zip'), + installDir, }) .then(() => { throw new Error('should have failed') @@ -50,12 +50,17 @@ describe('unzip', function () { }) it('can really unzip', function () { + const onProgress = sinon.stub().returns(undefined) + return unzip .start({ - downloadDestination: path.join('test', 'fixture', 'example.zip'), + zipFilePath: path.join('test', 'fixture', 'example.zip'), + installDir, + progress: { onProgress }, }) .then(() => { - return fs.statAsync(dest) + expect(onProgress).to.be.called + return fs.statAsync(installDir) }) }) }) diff --git a/cli/test/lib/tasks/verify_spec.js b/cli/test/lib/tasks/verify_spec.js index fd861a7b2d58..3bceaa08a683 100644 --- a/cli/test/lib/tasks/verify_spec.js +++ b/cli/test/lib/tasks/verify_spec.js @@ -12,18 +12,16 @@ const fs = require(`${lib}/fs`) const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) const xvfb = require(`${lib}/exec/xvfb`) -const info = require(`${lib}/tasks/info`) +const state = require(`${lib}/tasks/state`) const verify = require(`${lib}/tasks/verify`) const stdout = require('../../support/stdout') const normalize = require('../../support/normalize') const packageVersion = '1.2.3' -const executablePath = '/path/to/executable' -const executableDir = '/path/to/executable/dir' -const installationDir = info.getInstallationDir() +const executablePath = '/cache/Cypress/1.2.3/Cypress.app/executable' +const binaryDir = '/cache/Cypress/1.2.3/Cypress.app' -const LISTR_DELAY = 500 // for its animation const slice = (str) => { // strip answer and split by new lines @@ -42,37 +40,38 @@ const slice = (str) => { return str.join('\n') } -context('.verify', function () { +context('lib/tasks/verify', function () { require('mocha-banner').register() + beforeEach(function () { this.stdout = stdout.capture() - this.cpstderr = new EE() - this.cpstdout = new EE() - this.sandbox.stub(util, 'isCi').returns(false) - this.sandbox.stub(util, 'pkgVersion').returns(packageVersion) - this.sandbox.stub(os, 'platform').returns('darwin') - this.sandbox.stub(os, 'release').returns('test release') - this.ensureEmptyInstallationDir = () => { - return fs.removeAsync(installationDir) - .then(() => { - return info.ensureInstallationDir() - }) - } + this.cpstderr = sinon.stub(new EE()) + this.cpstderr.on.returns(undefined) + this.cpstdout = sinon.stub(new EE()) + this.cpstdout.on.returns(undefined) + + sinon.stub(util, 'isCi').returns(false) + sinon.stub(util, 'pkgVersion').returns(packageVersion) + os.platform.returns('darwin') this.spawnedProcess = _.extend(new EE(), { - unref: this.sandbox.stub(), + on: sinon.stub(), + unref: sinon.stub(), stderr: this.cpstderr, stdout: this.cpstdout, }) - this.sandbox.stub(cp, 'spawn').returns(this.spawnedProcess) - this.sandbox.stub(info, 'getPathToExecutable').returns(executablePath) - this.sandbox.stub(info, 'getPathToUserExecutableDir').returns(executableDir) - this.sandbox.stub(xvfb, 'start').resolves() - this.sandbox.stub(xvfb, 'stop').resolves() - this.sandbox.stub(xvfb, 'isNeeded').returns(false) - this.sandbox.stub(Promise, 'delay').resolves() - this.sandbox.stub(this.spawnedProcess, 'on') + this.spawnedProcess.on.withArgs('error') this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - return this.ensureEmptyInstallationDir() + + sinon.stub(cp, 'spawn').returns(this.spawnedProcess) + sinon.stub(state, 'getPathToExecutable').returns(executablePath) + sinon.stub(state, 'getBinaryDir').returns(binaryDir) + sinon.stub(xvfb, 'start').resolves() + sinon.stub(xvfb, 'stop').resolves() + sinon.stub(xvfb, 'isNeeded').returns(false) + sinon.stub(Promise.prototype, 'delay').resolves() + sinon.stub(state, 'writeBinaryVerifiedAsync').resolves() + sinon.stub(state, 'clearBinaryStateAsync').resolves() + sinon.stub(fs, 'realpathAsync') }) afterEach(function () { @@ -81,11 +80,8 @@ context('.verify', function () { it('logs error and exits when no version of Cypress is installed', function () { const ctx = this - - return info.writeInfoFileContents({}) - .then(() => { - return verify.start() - }) + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(false) + return verify.start() .then(() => { throw new Error('should have caught error') }) @@ -99,19 +95,14 @@ context('.verify', function () { }) }) - it('is noop when verifiedVersion matches expected', function () { + it('is noop when binary is already verified', function () { const ctx = this - // make it think the executable exists - this.sandbox.stub(fs, 'statAsync').resolves() - - return info.writeInfoFileContents({ - version: packageVersion, - verifiedVersion: packageVersion, - }) - .then(() => { - return verify.start() - }) + // make it think the executable exists and is verified + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(true) + return verify.start() .then(() => { // nothing should have been logged to stdout // since no verification took place @@ -123,19 +114,12 @@ context('.verify', function () { it('logs warning when installed version does not match verified version', function () { const ctx = this - - this.sandbox.stub(fs, 'statAsync') - .callThrough() - .withArgs(executablePath) - .resolves() - + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('bloop') // force this to throw to short circuit actually running smoke test - this.sandbox.stub(info, 'getVerifiedVersion').rejects(new Error) + sinon.stub(state, 'getBinaryVerifiedAsync').rejects(new Error()) - return info.writeInstalledVersion('bloop') - .then(() => { - return verify.start() - }) + return verify.start() .then(() => { throw new Error('should have caught error') }) @@ -149,11 +133,9 @@ context('.verify', function () { it('logs error and exits when executable cannot be found', function () { const ctx = this + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) - return info.writeInstalledVersion(packageVersion) - .then(() => { - return verify.start() - }) + return verify.start() .then(() => { throw new Error('should have caught error') }) @@ -169,19 +151,17 @@ context('.verify', function () { describe('with force: true', function () { beforeEach(function () { - this.sandbox.stub(fs, 'statAsync').resolves() - this.sandbox.stub(_, 'random').returns('222') - this.sandbox.stub(this.cpstdout, 'on').yieldsAsync('222') - - return info.writeInfoFileContents({ - version: packageVersion, - verifiedVersion: packageVersion, - }) + sinon.stub(_, 'random').returns('222') + this.cpstdout.on.yieldsAsync('222') }) it('shows full path to executable when verifying', function () { const ctx = this + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) + return verify.start({ force: true }) .then(() => { expect(cp.spawn).to.be.calledWith(executablePath, [ @@ -189,13 +169,6 @@ context('.verify', function () { '--ping=222', ]) }) - .then(() => { - return info.getVerifiedVersion() - }) - .then((vv) => { - expect(vv).to.eq(packageVersion) - }) - .delay(LISTR_DELAY) .then(() => { snapshot( 'verification with executable', @@ -206,11 +179,13 @@ context('.verify', function () { it('clears verified version from state if verification fails', function () { const ctx = this - const stderr = 'an error about dependencies' - this.sandbox.stub(this.cpstderr, 'on').withArgs('data').yields(stderr) + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(true) + this.cpstderr.on.withArgs('data').yields(stderr) this.spawnedProcess.on.withArgs('close').yieldsAsync(1) return verify.start({ force: true }) @@ -221,44 +196,34 @@ context('.verify', function () { 'fails verifying Cypress', normalize(slice(ctx.stdout.toString())) ) - - return info.getVerifiedVersion() }) - .then((verifiedVersion) => { - expect(verifiedVersion).to.be.null + .then(() => { + expect(state.clearBinaryStateAsync).to.be.called + expect(state.writeBinaryVerifiedAsync).to.not.be.called }) }) }) describe('smoke test with DEBUG output', function () { beforeEach(function () { - this.sandbox.stub(fs, 'statAsync').resolves() - this.sandbox.stub(_, 'random').returns('222') + sinon.stub(fs, 'statAsync').resolves() + sinon.stub(_, 'random').returns('222') const stdoutWithDebugOutput = stripIndent` some debug output date: more debug output 222 after that more text ` - this.sandbox.stub(this.cpstdout, 'on').yieldsAsync(stdoutWithDebugOutput) + this.cpstdout.on.yieldsAsync(stdoutWithDebugOutput) }) it('finds ping value in the verbose output', function () { const ctx = this + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) - return info.writeInfoFileContents({ - version: packageVersion, - }) - .then(() => { - return verify.start() - }) - .then(() => { - return info.getVerifiedVersion() - }) - .then((vv) => { - expect(vv).to.eq(packageVersion) - }) - .delay(LISTR_DELAY) + return verify.start() .then(() => { snapshot( 'verbose stdout output', @@ -270,27 +235,18 @@ context('.verify', function () { describe('smoke test', function () { beforeEach(function () { - this.sandbox.stub(fs, 'statAsync').resolves() - this.sandbox.stub(_, 'random').returns('222') - this.sandbox.stub(this.cpstdout, 'on').yieldsAsync('222') + sinon.stub(fs, 'statAsync').resolves() + sinon.stub(_, 'random').returns('222') + this.cpstdout.on.yieldsAsync('222') }) it('logs and runs when no version has been verified', function () { const ctx = this + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) - return info.writeInfoFileContents({ - version: packageVersion, - }) - .then(() => { - return verify.start() - }) - .then(() => { - return info.getVerifiedVersion() - }) - .then((vv) => { - expect(vv).to.eq(packageVersion) - }) - .delay(LISTR_DELAY) + return verify.start() .then(() => { snapshot( 'no existing version verified', @@ -301,21 +257,10 @@ context('.verify', function () { it('logs and runs when current version has not been verified', function () { const ctx = this - - return info.writeInfoFileContents({ - version: packageVersion, - verifiedVersion: 'different version', - }) - .then(() => { - return verify.start() - }) - .then(() => { - return info.getVerifiedVersion() - }) - .then((vv) => { - expect(vv).to.eq(packageVersion) - }) - .delay(LISTR_DELAY) + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('different version') + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) + return verify.start() .then(() => { snapshot( 'current version has not been verified', @@ -326,21 +271,11 @@ context('.verify', function () { it('logs and runs when installed version is different than verified version', function () { const ctx = this + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('9.8.7') + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) - return info.writeInfoFileContents({ - version: '9.8.7', - verifiedVersion: packageVersion, - }) - .then(() => { - return verify.start() - }) - .then(() => { - return info.getVerifiedVersion() - }) - .then((vv) => { - expect(vv).to.eq('9.8.7') - }) - .delay(LISTR_DELAY) + return verify.start() .then(() => { snapshot( 'current version has not been verified', @@ -351,17 +286,13 @@ context('.verify', function () { it('turns off Opening Cypress...', function () { const ctx = this + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('different version') + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(true) - return info.writeInfoFileContents({ - version: packageVersion, - verifiedVersion: 'different version', - }) - .then(() => { - return verify.start({ - welcomeMessage: false, - }) + return verify.start({ + welcomeMessage: false, }) - .delay(LISTR_DELAY) .then(() => { snapshot( 'no welcome message', @@ -373,11 +304,9 @@ context('.verify', function () { describe('on linux', function () { beforeEach(function () { xvfb.isNeeded.returns(true) - - return info.writeInfoFileContents({ - version: packageVersion, - verifiedVersion: 'different version', - }) + sinon.stub(fs, 'pathExistsAsync').withArgs(executablePath).resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) }) it('starts xvfb', function () { @@ -417,14 +346,12 @@ context('.verify', function () { describe('when running in CI', function () { beforeEach(function () { + sinon.stub(fs, 'pathExistsAsync').resolves(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) util.isCi.returns(true) - return info.writeInfoFileContents({ - version: packageVersion, - }) - .then(() => { - return verify.start({ force: true }) - }) + return verify.start({ force: true }) }) it('uses verbose renderer', function () { @@ -434,5 +361,60 @@ context('.verify', function () { ) }) }) + + describe('when env var CYPRESS_RUN_BINARY', function () { + beforeEach(function () { + xvfb.isNeeded.returns(true) + sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(packageVersion) + sinon.stub(state, 'getBinaryVerifiedAsync').resolves(false) + sinon.stub(util, 'isExecutableAsync').resolves(true) + }) + + it('can validate and use executable', function () { + const envBinaryPath = '/custom/Contents/MacOS/Cypress' + const realEnvBinaryPath = `/real${envBinaryPath}` + + fs.realpathAsync.resolves(realEnvBinaryPath) + state.getPathToExecutable.restore() + + sinon.stub(state, 'getPathToExecutable') + .withArgs('/real/custom') + .returns(realEnvBinaryPath) + + sinon.stub(fs, 'pathExistsAsync') + .withArgs(realEnvBinaryPath) + .resolves(true) + + process.env.CYPRESS_RUN_BINARY = envBinaryPath + return verify.start() + .then(() => { + expect(cp.spawn.firstCall.args[0]).to.equal(realEnvBinaryPath) + snapshot('valid CYPRESS_RUN_BINARY', normalize(this.stdout.toString())) + }) + }) + + + ;['darwin', 'linux', 'win32'].forEach((platform) => it('can log error to user', function () { + os.platform.returns(platform) + const envBinaryPath = '/custom/' + const realEnvBinaryPath = `/real${envBinaryPath}` + + fs.realpathAsync.resolves(realEnvBinaryPath) + state.getPathToExecutable.restore() + + process.env.CYPRESS_RUN_BINARY = envBinaryPath + return verify.start() + .then(() => { throw new Error('Should have thrown') }) + .catch((err) => { + logger.error(err) + snapshot( + `${platform}: error when invalid CYPRESS_RUN_BINARY`, + normalize(this.stdout.toString()) + ) + }) + + + })) + }) }) }) diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index dbf353b16779..0d45fe3bf938 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -1,15 +1,18 @@ require('../spec_helper') +const os = require('os') +const tty = require('tty') const snapshot = require('snap-shot-it') const supportsColor = require('supports-color') +const proxyquire = require('proxyquire') const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) -describe('util', function () { - beforeEach(function () { - this.sandbox.stub(process, 'exit') - this.sandbox.stub(logger, 'error') +describe('util', () => { + beforeEach(() => { + sinon.stub(process, 'exit') + sinon.stub(logger, 'error') }) context('.stdoutLineMatches', () => { @@ -108,35 +111,201 @@ describe('util', function () { }) }) - context('.supportsColor', function () { - it('is true on obj return for stderr', function () { - const obj = {} - this.sandbox.stub(supportsColor, 'stderr').value(obj) + context('.supportsColor', () => { + it('is true on obj return for stdout and stderr', () => { + sinon.stub(supportsColor, 'stdout').value({}) + sinon.stub(supportsColor, 'stderr').value({}) expect(util.supportsColor()).to.be.true }) - it('is false on false return for stderr', function () { - this.sandbox.stub(supportsColor, 'stderr').value(false) + it('is false on false return for stdout', () => { + delete process.env.CI + + sinon.stub(supportsColor, 'stdout').value(false) + sinon.stub(supportsColor, 'stderr').value({}) + + expect(util.supportsColor()).to.be.false + }) + + it('is false on false return for stderr', () => { + delete process.env.CI + + sinon.stub(supportsColor, 'stdout').value({}) + sinon.stub(supportsColor, 'stderr').value(false) expect(util.supportsColor()).to.be.false }) + + it('is true when running in CI', () => { + process.env.CI = '1' + sinon.stub(supportsColor, 'stdout').value(false) + + expect(util.supportsColor()).to.be.true + }) + + it('is false when NO_COLOR has been set', () => { + process.env.CI = '1' + process.env.NO_COLOR = '1' + sinon.stub(supportsColor, 'stdout').value({}) + sinon.stub(supportsColor, 'stderr').value({}) + + expect(util.supportsColor()).to.be.FALSE + }) + }) + + context('.getEnvOverrides', () => { + it('returns object with colors + process overrides', () => { + // shouldn't be stubbing 'what we own' but its easiest in this case + sinon.stub(util, 'supportsColor').returns(true) + sinon.stub(tty, 'isatty').returns(true) + + expect(util.getEnvOverrides()).to.deep.eq({ + FORCE_STDIN_TTY: '1', + FORCE_STDOUT_TTY: '1', + FORCE_STDERR_TTY: '1', + FORCE_COLOR: '1', + DEBUG_COLORS: '1', + MOCHA_COLORS: '1', + }) + + util.supportsColor.returns(false) + tty.isatty.returns(false) + + expect(util.getEnvOverrides()).to.deep.eq({ + FORCE_STDIN_TTY: '0', + FORCE_STDOUT_TTY: '0', + FORCE_STDERR_TTY: '0', + FORCE_COLOR: '0', + DEBUG_COLORS: '0', + }) + }) + }) + + context('.getForceTty', () => { + it('forces when each stream is a tty', () => { + sinon.stub(tty, 'isatty') + .withArgs(0).returns(true) + .withArgs(1).returns(true) + .withArgs(2).returns(true) + + expect(util.getForceTty()).to.deep.eq({ + FORCE_STDIN_TTY: true, + FORCE_STDOUT_TTY: true, + FORCE_STDERR_TTY: true, + }) + + tty.isatty + .withArgs(0).returns(false) + .withArgs(1).returns(false) + .withArgs(2).returns(false) + + expect(util.getForceTty()).to.deep.eq({ + FORCE_STDIN_TTY: false, + FORCE_STDOUT_TTY: false, + FORCE_STDERR_TTY: false, + }) + }) + }) + + context('.exit', () => { + it('calls process.exit', () => { + process.exit.withArgs(2).withArgs(0) + util.exit(2) + util.exit(0) + }) }) - it('.exit', function () { - util.exit(2) - expect(process.exit).to.be.calledWith(2) + context('.logErrorExit1', () => { + it('calls logger.error and process.exit', () => { + const err = new Error('foo') - util.exit(0) - expect(process.exit).to.be.calledWith(0) + logger.error.withArgs('foo') + process.exit.withArgs(1) + + util.logErrorExit1(err) + }) + }) + + describe('.isSemver', () => { + it('is true with 3-digit version', () => { + expect(util.isSemver('1.2.3')).to.equal(true) + }) + it('is true with 2-digit version', () => { + expect(util.isSemver('1.2')).to.equal(true) + }) + it('is true with 1-digit version', () => { + expect(util.isSemver('1')).to.equal(true) + }) + it('is false with URL', () => { + expect(util.isSemver('www.cypress.io/download/1.2.3')).to.equal(false) + }) + it('is false with file path', () => { + expect(util.isSemver('0/path/1.2.3/mypath/2.3')).to.equal(false) + }) }) - it('.logErrorExit1', function () { - const err = new Error('foo') + context('.printNodeOptions', () => { + describe('NODE_OPTIONS is not set', () => { + + it('does nothing if debug is not enabled', () => { + const log = sinon.spy() + log.enabled = false + util.printNodeOptions(log) + expect(log).not.have.been.called + }) - util.logErrorExit1(err) + it('prints message when debug is enabled', () => { + const log = sinon.spy() + log.enabled = true + util.printNodeOptions(log) + expect(log).to.be.calledWith('NODE_OPTIONS is not set') + }) + }) - expect(process.exit).to.be.calledWith(1) - expect(logger.error).to.be.calledWith('foo') + describe('NODE_OPTIONS is set', () => { + beforeEach(() => { + process.env.NODE_OPTIONS = 'foo' + }) + + it('does nothing if debug is not enabled', () => { + const log = sinon.spy() + log.enabled = false + util.printNodeOptions(log) + expect(log).not.have.been.called + }) + + it('prints value when debug is enabled', () => { + const log = sinon.spy() + log.enabled = true + util.printNodeOptions(log) + expect(log).to.be.calledWith('NODE_OPTIONS=%s', 'foo') + }) + }) + + describe('.getOsVersionAsync', () => { + let util + let getos = sinon.stub().resolves(['distro-release']) + beforeEach(() => { + util = proxyquire(`${lib}/util`, { getos }) + }) + it('calls os.release on non-linux', () => { + os.platform.returns('darwin') + os.release.returns('some-release') + util.getOsVersionAsync() + .then(() => { + expect(os.release).to.be.called + expect(getos).to.not.be.called + }) + }) + it('NOT calls os.release on linux', () => { + os.platform.returns('linux') + util.getOsVersionAsync() + .then(() => { + expect(os.release).to.not.be.called + expect(getos).to.be.called + }) + }) + }) }) }) diff --git a/cli/test/spec_helper.js b/cli/test/spec_helper.js index 64783a651a46..2560b9ff3b01 100644 --- a/cli/test/spec_helper.js +++ b/cli/test/spec_helper.js @@ -1,7 +1,11 @@ +const _ = require('lodash') +const os = require('os') const path = require('path') const sinon = require('sinon') const Promise = require('bluebird') +const util = require('../lib/util') +global.sinon = sinon global.expect = require('chai').expect global.lib = path.join(__dirname, '..', 'lib') @@ -9,10 +13,74 @@ require('chai') .use(require('@cypress/sinon-chai')) .use(require('chai-string')) +sinon.usingPromise(Promise) + +delete process.env.CYPRESS_RUN_BINARY +delete process.env.CYPRESS_INSTALL_BINARY +delete process.env.CYPRESS_CACHE_FOLDER +delete process.env.CYPRESS_BINARY_VERSION +delete process.env.CYPRESS_SKIP_BINARY_INSTALL +delete process.env.DISPLAY + +const env = _.clone(process.env) + +function throwIfFnNotStubbed (stub, method) { + const sig = `.${method}(...)` + + stub.callsFake(function (...args) { + const err = new Error(`${sig} was called without being stubbed. + + ${sig} was called with arguments: + + ${args.map(JSON.stringify).join(', ')} + `) + + err.stack = _ + .chain(err.stack) + .split('\n') + .reject((str) => _.includes(str, 'sinon')) + .join('\n') + .value() + + throw err + }) +} + +const $stub = sinon.stub +sinon.stub = function (obj, method) { + /* eslint-disable prefer-rest-params */ + const stub = $stub.apply(this, arguments) + + let fns = [method] + + if (arguments.length === 1) { + fns = _.functions(obj) + } + + if (arguments.length === 0) { + throwIfFnNotStubbed(stub, '[anonymous function]') + + return stub + } + + fns.forEach((name) => { + const fn = obj[name] + + if (_.isFunction(fn)) { + throwIfFnNotStubbed(fn, name) + } + }) + + return stub +} + beforeEach(function () { - this.sandbox = sinon.sandbox.create().usingPromise(Promise) + sinon.stub(os, 'platform') + sinon.stub(os, 'release') + sinon.stub(util, 'getOsVersionAsync').resolves('Foo-OsVersion') }) afterEach(function () { - this.sandbox.restore() + process.env = _.clone(env) + sinon.restore() }) diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index f230a272bba9..42c401fc088a 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -7,13 +7,14 @@ // TypeScript Version: 2.5 // Updated by the Cypress team: https://www.cypress.io/about/ -/// -/// +/// +/// +/// + /// /// /// /// -/// /// /// /// @@ -138,7 +139,7 @@ declare namespace Cypress { env(key: string, value: any): void env(object: ObjectLike): void - log(options: Partial): void + log(options: Partial): Log /** * @see https://on.cypress.io/api/commands @@ -173,7 +174,24 @@ declare namespace Cypress { defaults(options: Partial): void } + /** + * @see https://on.cypress.io/api/screenshot-api + */ + Screenshot: { + defaults(options: Partial): void + } + + /** + * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. + * @see https://on.cypress.io/catalog-of-events#App-Events + */ on: Actions + + /** + * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. + * @see https://on.cypress.io/catalog-of-events#App-Events + */ + off: Actions } /** @@ -421,6 +439,7 @@ declare namespace Cypress { */ filter(selector: K, options?: Partial): Chainable> // automatically returns the correct HTMLElement type filter(selector: string, options?: Partial): Chainable> + filter(fn: (index: number, element: E) => boolean, options?: Partial): Chainable> /** * Get the descendent DOM elements of a specific selector. @@ -608,6 +627,12 @@ declare namespace Cypress { */ on: Actions + /** + * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. + * @see https://on.cypress.io/catalog-of-events#App-Events + */ + off: Actions + /** * Get the parent DOM element of a set of DOM elements. * @@ -733,7 +758,7 @@ declare namespace Cypress { * @see https://on.cypress.io/screenshot */ screenshot(options?: Partial): Chainable - screenshot(fileName: string, options?: Partial): Chainable + screenshot(fileName: string, options?: Partial): Chainable /** * Scroll an element into view. @@ -819,6 +844,13 @@ declare namespace Cypress { spread(fn: (...args: any[]) => S): Chainable spread(fn: (...args: any[]) => void): Chainable + /** + * Run a task in Node via the plugins file. + * + * @see https://on.cypress.io/task + */ + task(event: string, arg?: any, options?: Partial): Chainable + /** * Enables you to work with the subject yielded from the previous command. * @@ -1239,6 +1271,27 @@ declare namespace Cypress { onAbort(...args: any[]): void } + interface Dimensions { + x: number + y: number + width: number + height: number + } + + interface ScreenshotOptions { + blackout: string[] + capture: 'runner' | 'viewport' | 'fullPage' + clip: Dimensions + disableTimersAndAnimations: boolean + scale: boolean + beforeScreenshot(doc: Document): void + afterScreenshot(doc: Document): void + } + + interface ScreenshotDefaultsOptions extends ScreenshotOptions { + screenshotOnRunFailure: boolean + } + interface ScrollToOptions extends Loggable, Timeoutable { /** * Scrolls over the duration (in ms) @@ -1251,7 +1304,16 @@ declare namespace Cypress { * * @default 'swing' */ - easing: 'swing' | 'linear' + easing: 'swing' | 'linear', + } + + interface ScrollIntoViewOptions extends ScrollToOptions { + /** + * Amount to scroll after the element has been scrolled into view + * + * @default {top: 0, left: 0} + */ + offset: Offset } interface SelectOptions extends Loggable, Timeoutable, Forceable { @@ -3106,15 +3168,31 @@ declare namespace Cypress { stderr: string } + interface LogAttrs { + url: string + consoleProps: ObjectLike + } + interface Log { - $el: any + end(): Log + finish(): void + get(attr: K): LogConfig[K] + get(): LogConfig + set(key: K, value: LogConfig[K]): Log + set(options: Partial): Log + snapshot(name?: string, options?: { at?: number, next: string }): Log + } + + interface LogConfig { + /** The JQuery element for the command. This will highlight the command in the main window when debugging */ + $el: JQuery /** Allows the name of the command to be overwritten */ name: string /** Override *name* for display purposes only */ displayName: string - message: any + message: any[] /** Return an object that will be printed in the dev tools console */ - consoleProps(): any + consoleProps(): ObjectLike } interface Response { @@ -3164,6 +3242,10 @@ declare namespace Cypress { type Encodings = 'ascii' | 'base64' | 'binary' | 'hex' | 'latin1' | 'utf8' | 'utf-8' | 'ucs2' | 'ucs-2' | 'utf16le' | 'utf-16le' type PositionType = "topLeft" | "top" | "topRight" | "left" | "center" | "right" | "bottomLeft" | "bottom" | "bottomRight" type ViewportPreset = 'macbook-15' | 'macbook-13' | 'macbook-11' | 'ipad-2' | 'ipad-mini' | 'iphone-6+' | 'iphone-6' | 'iphone-5' | 'iphone-4' | 'iphone-3' + interface Offset { + top: number, + left: number + } // Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766 type Diff = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T] diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index d4f73fdba565..90a3594df763 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -50,12 +50,26 @@ namespace CypressCommandsTests { } namespace CypressLogsTest { - Cypress.log({ - $el: 'foo', + const log = Cypress.log({ + $el: Cypress.$('body'), name: 'MyCommand', displayName: 'My Command', message: ['foo', 'bar'], - }) + consoleProps: () => { + return { + foo: 'bar', + } + }, + }) + .set('$el', Cypress.$('body')) + .set({ name: 'MyCommand' }) + .snapshot() + .snapshot('before') + .snapshot('before', { next: 'after' }) + + log.get() // $ExpectType LogConfig + log.get('name') // $ExpectType string + log.get('$el') // $ExpectType JQuery } cy.wrap({ foo: [1, 2, 3] }) @@ -171,3 +185,36 @@ cy .then(subject => { subject // $ExpectType undefined }) + +namespace CypressOnTests { + Cypress.on('uncaught:exception', (error, runnable) => { + error // $ExpectType Error + runnable // $ExpectType IRunnable + }) + + cy.on('uncaught:exception', (error, runnable) => { + error // $ExpectType Error + runnable // $ExpectType IRunnable + }) +} + +namespace CypressOffTests { + Cypress.off('uncaught:exception', (error, runnable) => { + error // $ExpectType Error + runnable // $ExpectType IRunnable + }) + + cy.off('uncaught:exception', (error, runnable) => { + error // $ExpectType Error + runnable // $ExpectType IRunnable + }) +} + +namespace CypressFilterTests { + cy.get('#id') + .filter((index: number, element: HTMLDivElement) => { + index // $ExpectType number + element // $ExpectType HTMLDivElement + return true + }) +} diff --git a/cli/types/tests/kitchen-sink.ts b/cli/types/tests/kitchen-sink.ts index f60e3f928e61..9bb00ec6a5d0 100644 --- a/cli/types/tests/kitchen-sink.ts +++ b/cli/types/tests/kitchen-sink.ts @@ -429,6 +429,13 @@ describe('Kitchen Sink', function() { // Cypress knows to scroll to the right and down cy.get('#scroll-both button').scrollIntoView() .should('be.visible') + + // We can set scroll duration + cy.get('#scroll-both button').scrollIntoView({ + duration: 1000, + easing: 'swing', + offset: {top: 0, left: 0} + }) }) it('cy.scrollTo() - scroll the window or element to a position', function() { @@ -776,7 +783,20 @@ describe('Kitchen Sink', function() { it('cy.screenshot() - take a screenshot', function() { // https://on.cypress.io/screenshot - cy.screenshot('my-image') + cy.screenshot('my-image', { + blackout: ['.foo'], + capture: 'viewport', + clip: { x: 0, y: 0, width: 200, height: 200 }, + disableTimersAndAnimations: true, + scale: true, + beforeScreenshot() {}, + afterScreenshot() {}, + }) + }) + + it('cy.task() - run a task', function() { + // https://on.cypress.io/task + cy.task('my-task', 'my-arg') }) it('cy.wrap() - wrap an object', function() { @@ -1467,6 +1487,22 @@ describe('Kitchen Sink', function() { }) }) }) + + context('Cypress.Screenshot', function() { + // https://on.cypress.io/api/screenshot-api + it('Cypress.Screenshot.defaults() - change default config of screenshots', function() { + Cypress.Screenshot.defaults({ + blackout: ['.foo'], + capture: 'viewport', + clip: { x: 0, y: 0, width: 200, height: 200 }, + scale: false, + disableTimersAndAnimations: true, + screenshotOnRunFailure: true, + beforeScreenshot() { }, + afterScreenshot() { }, + }) + }) + }) }) cy.wrap('foo').then(subject => { diff --git a/package.json b/package.json index 90ca0cd576da..cd61b69335d8 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,10 @@ "console.table": "^0.9.1", "debug": "3.1.0", "del": "^3.0.0", - "deps-ok": "^1.2.0", + "deps-ok": "^1.4.1", "electron-osx-sign": "^0.4.6", "eslint": "4.13.1", + "eslint-plugin-cypress": "^2.0.1", "eslint-plugin-cypress-dev": "^1.1.1", "eslint-plugin-mocha": "^4.11.0", "eslint-plugin-react": "^7.3.0", @@ -122,5 +123,20 @@ }, "bugs": { "url": "https://github.com/cypress-io/cypress/issues" - } + }, + "keywords": [ + "browser", + "cypress", + "cypress.io", + "automation", + "end-to-end", + "e2e", + "integration", + "mocks", + "test", + "testing", + "runner", + "spies", + "stubs" + ] } diff --git a/packages/desktop-gui/cypress/fixtures/config.json b/packages/desktop-gui/cypress/fixtures/config.json index ce51b035ff54..094ab3703432 100644 --- a/packages/desktop-gui/cypress/fixtures/config.json +++ b/packages/desktop-gui/cypress/fixtures/config.json @@ -31,12 +31,12 @@ "env": { }, - "blacklistHosts": ["www.google-analytics.com", "hotjar.com"], + "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", - "integrationExampleFile": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration/example_spec.js", - "integrationExampleName": "example_spec.js", + "integrationExamplePath": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration/examples", + "integrationExampleName": "examples", "integrationFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration", "isHeadless": false, "isNewProject": false, @@ -67,7 +67,66 @@ "name": "integration", "children": [ { - "name": "example_spec.js" + "name": "examples", + "children": [ + { + "name": "actions.spec.js" + }, + { + "name": "aliasing.spec.js" + }, + { + "name": "assertions.spec.js" + }, + { + "name": "connectors.spec.js" + }, + { + "name": "cookies.spec.js" + }, + { + "name": "cypress_api.spec.js" + }, + { + "name": "files.spec.js" + }, + { + "name": "local_storage.spec.js" + }, + { + "name": "location.spec.js" + }, + { + "name": "misc.spec.js" + }, + { + "name": "navigation.spec.js" + }, + { + "name": "network_requests.spec.js" + }, + { + "name": "querying.spec.js" + }, + { + "name": "spies_stubs_clocks.spec.js" + }, + { + "name": "traversal.spec.js" + }, + { + "name": "utilities.spec.js" + }, + { + "name": "viewport.spec.js" + }, + { + "name": "waiting.spec.js" + }, + { + "name": "window.spec.js" + } + ] } ] }, @@ -92,6 +151,14 @@ "name": "index.js" } ] + }, + { + "name": "plugins", + "children": [ + { + "name": "index.js" + } + ] } ] } diff --git a/packages/desktop-gui/cypress/fixtures/projects.json b/packages/desktop-gui/cypress/fixtures/projects.json index 258b803f81a7..e2f66948dd57 100644 --- a/packages/desktop-gui/cypress/fixtures/projects.json +++ b/packages/desktop-gui/cypress/fixtures/projects.json @@ -1,6 +1,6 @@ [ { - "id": "pol9712jdha0wef082h98ha-8f98h3", + "id": "3d897a", "path": "/Users/Jane/Projects/My-Fake-Project" }, { diff --git a/packages/desktop-gui/cypress/fixtures/runs.json b/packages/desktop-gui/cypress/fixtures/runs.json index 6557eec120f3..a2e2efac3fce 100644 --- a/packages/desktop-gui/cypress/fixtures/runs.json +++ b/packages/desktop-gui/cypress/fixtures/runs.json @@ -1,143 +1,630 @@ [ { - "buildNumber": "1891", - "ciProvider": "CircleCI", - "ciUrl": "https://circleci.com/gh/cypress-io/cypress-core-example/140", - "commitAuthorEmail": "julie@devs.com", - "commitAuthorName": "Julie Pearson", - "commitBranch": "search-todos", - "commitMessage": "remove listings from search results on clear", - "commitSha": "abc1234", - "createdAt": "2016-05-13T02:35:12.748Z", - "expectedInstances": 1, + "buildNumber": 1891, + "ci": { + "buildNumber": "140", + "provider": "CircleCI", + "url": "https://circleci.com/gh/cypress-io/cypress-core-example/140" + }, + "commit": { + "authorEmail": "julie@devs.com", + "authorName": "Julie Pearson", + "branch": "search-todos", + "message": "remove listings from search results on clear", + "sha": "abc1234", + "url": "https://github.com/cypress-io/cypress-core-example/commit/abc1234" + }, + "createdAt": "2016-12-19T14:12:57.328Z", + "cypressVersion": "0.18.5", + "completedAt": null, + "failedTests": [ + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "id": "175e807c-ce85-5f94-938c-e2e30b090633", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r2", + "timings": { + "before each": [ + { + "hookId": "h5a7b63e34b846429cd32b596", + "fnDuration": 6, + "afterFnDuration": 55 + } + ], + "lifecycle": 15 + }, + "title": [ + "Projects", + "lists projects", + "displays all" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + }, + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "failedFromHookId": "h1", + "id": "ef0d934e-a247-5e60-b801-3c5be5aa8796", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r1", + "timings": { + "before each": [ + { + "afterFnDuration": 55, + "fnDuration": 6, + "hookId": "h5a7b63e34b846429cd32b596" + } + ], + "lifecycle": 15 + }, + "title": [ + "Users List", + "lists users", + "displays users roles (\u001b[4m\u001b[1mTests Starting\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[0m\u001b[0m\n\u001b[0m Kitchen Sink\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - assert that is correct\u001b[0m\u001b[31m (358ms)\u001b[0m\n\u001b[0m Querying\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.get() - query DOM elements\u001b[0m\u001b[31m (84ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.contains() - query DOM elements with matching content\u001b[0m\u001b[31m (145ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .within() - query DOM elements within a specific element\u001b[0m\u001b[33m (63ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.root() - query the root DOM element\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[0m Traversal\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .children() - get child DOM elements\u001b[0m\u001b[33m (46ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .closest() - get closest ancestor DOM element\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .eq() - get a DOM element at a specific index\u001b[0m\u001b[33m (61ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .filter() - get DOM elements that match the selector\u001b[0m\u001b[33m (51ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .find() - get descendant DOM elements of the selector\u001b[0m\u001b[31m (82ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .first() - get first DOM element\u001b[0m\u001b[31m (86ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .last() - get last DOM element\u001b[0m\u001b[33m (40ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .next() - get next sibling DOM element\u001b[0m\u001b[33m (42ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextAll() - get all next sibling DOM elements\u001b[0m\u001b[33m (58ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextUntil() - get next sibling DOM elements until next el\u001b[0m\u001b[33m (44ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .not() - remove DOM elements from set of DOM elements\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parent() - get parent DOM element from DOM elements\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parents() - get parent DOM elements from DOM elements\u001b[0m\u001b[33m (52ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parentsUntil() - get parent DOM elements from DOM elements until el\u001b[0m\u001b[33m (54ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prev() - get previous sibling DOM element\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevAll() - get all previous sibling DOM elements\u001b[0m\u001b[33m (48ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevUntil() - get all previous sibling DOM elements until el\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .siblings() - get all sibling DOM elements\u001b[0m\u001b[33m (38ms)\u001b[0m\n\u001b[0m Actions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .type() - type into a DOM element\u001b[0m\u001b[31m (4045ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .focus() - focus on a DOM element\u001b[0m\u001b[31m (118ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .blur() - blur off a DOM element\u001b[0m\u001b[31m (537ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .clear() - clears an input or textarea element\u001b[0m\u001b[31m (825ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .submit() - submit a form\u001b[0m\u001b[31m (423ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .click() - click on a DOM element\u001b[0m\u001b[31m (2555ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .dblclick() - double click on a DOM element\u001b[0m\u001b[31m (120ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.check() - check a checkbox or radio element\u001b[0m\u001b[31m (1968ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .uncheck() - uncheck a checkbox element\u001b[0m\u001b[31m (1637ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .select() - select an option in a <select> element\u001b[0m\u001b[31m (1092ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .scrollIntoView() - scroll an element into view\u001b[0m\u001b[31m (148ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.scrollTo() - scroll the window or element to a position\u001b[0m\u001b[31m (2043ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .trigger() - trigger an event on a DOM element\u001b[0m\u001b[31m (114ms)\u001b[0m\n\u001b[0m Window\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.window() - get the global window object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.document() - get the document object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.title() - get the title\u001b[0m\n\u001b[0m Viewport\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.viewport() - set the viewport size and dimension\u001b[0m\u001b[31m (2771ms)\u001b[0m\n\u001b[0m Location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.hash() - get the current URL hash\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.location() - get window.location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.url() - get the current URL\u001b[0m\n\u001b[0m Navigation\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.go() - go back or forward in the browser's history\u001b[0m\u001b[31m (664ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.reload() - reload the page\u001b[0m\u001b[31m (613ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.visit() - visit a remote url\u001b[0m\u001b[31m (378ms)\u001b[0m\n\u001b[0m Assertions\u001b[0m\n\u001b[0m Implicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - make an assertion about the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .and() - chain multiple assertions together\u001b[0m\n\u001b[0m Explicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - assert shape of an object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - make an assertion about a specified subject\u001b[0m\n\u001b[0m Misc\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .end() - end the command chain\u001b[0m\u001b[31m (269ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.exec() - execute a system command\u001b[0m\u001b[31m (732ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.focused() - get the DOM element that has focus\u001b[0m\u001b[31m (426ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.screenshot() - take a screenshot\u001b[0m\u001b[31m (234ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wrap() - wrap an object\u001b[0m\n\u001b[0m Connectors\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .each() - iterate over an array of elements\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .its() - get properties on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .invoke() - invoke a function on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .spread() - spread an array as individual args to callback function\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .then() - invoke a callback function with the current subject\u001b[0m\u001b[33m (67ms)\u001b[0m\n\u001b[0m Aliasing\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .as() - alias a route or DOM element for later use\u001b[0m\u001b[31m (240ms)\u001b[0m\n\u001b[0m Waiting\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wait() - wait for a specific amount of time\u001b[0m\u001b[31m (4726ms)\u001b[0m\n\u001b[0m Network Requests\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.server() - control behavior of network requests and responses\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.request() - make an XHR request\u001b[0m\u001b[31m (457ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.route() - route responses to matching requests\u001b[0m\u001b[31m (1565ms)\u001b[0m\n\u001b[0m Files\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.fixture() - load a fixture\u001b[0m\u001b[31m (444ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.readFile() - read a files contents\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.writeFile() - write to a file\u001b[0m\u001b[31m (146ms)\u001b[0m\n\u001b[0m Local Storage\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearLocalStorage() - clear all data in local storage\u001b[0m\u001b[31m (402ms)\u001b[0m\n\u001b[0m Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookie() - get a browser cookie\u001b[0m\u001b[31m (168ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookies() - get browser cookies\u001b[0m\u001b[31m (184ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.setCookie() - set a browser cookie\u001b[0m\u001b[33m (39ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookie() - clear a browser cookie\u001b[0m\u001b[31m (189ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookies() - clear browser cookies\u001b[0m\u001b[31m (197ms)\u001b[0m\n\u001b[0m Spies, Stubs, and Clock\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.spy() - wrap a method in a spy\u001b[0m\u001b[31m (472ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.stub() - create a stub and/or replace a function with a stub\u001b[0m\u001b[31m (225ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clock() - control time in the browser\u001b[0m\u001b[31m (383ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.tick() - move time in the browser\u001b[0m\u001b[31m (616ms)\u001b[0m\n\u001b[0m Utilities\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress._.method() - call a lodash method\u001b[0m\u001b[31m (154ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.$(selector) - call a jQuery method\u001b[0m\u001b[31m (188ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.moment() - format or parse dates using a moment method\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Blob.method() - blob utilities and base64 string conversion\u001b[0m\u001b[31m (321ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m new Cypress.Promise(function) - instantiate a bluebird promise\u001b[0m\u001b[31m (1009ms)\u001b[0m\n\u001b[0m Cypress.config()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.config() - get and set configuration options\u001b[0m\u001b[33m (59ms)\u001b[0m\n\u001b[0m Cypress.env()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.env() - get environment variables\u001b[0m\n\u001b[0m Cypress.Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.debug() - enable or disable debugging\u001b[0m\u001b[31m (79ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.preserveOnce() - preserve cookies by key\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.defaults() - set defaults for all cookies\u001b[0m\n\u001b[0m Cypress.dom\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.dom.isHidden() - determine if a DOM element is hidden\u001b[0m\n\u001b[0m Cypress.Server\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Server.defaults() - change default config of server\u001b[0m\n\n\n\u001b[92m \u001b[0m\u001b[32m 90 passing\u001b[0m\u001b[90m (1m)\u001b[0m\n\n\n\u001b[32m (\u001b[4m\u001b[1mTests Finished\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[37m - Tests: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Passes: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Failures: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Pending: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Duration: \u001b[39m\u001b[32m1 minute, 11 seconds\u001b[39m\n\u001b[37m - Screenshots: \u001b[39m\u001b[32m1\u001b[39m\n\u001b[37m - Video Recorded: \u001b[39m\u001b[32mfalse\u001b[39m\n\u001b[37m - Cypress Version: \u001b[39m\u001b[32m1.4.1\u001b[39m\n\n\n\u001b[33m (\u001b[4m\u001b[1mScreenshots\u001b[22m\u001b[24m)\u001b[39m\n\n - /tmp/repo/cypress/screenshots/my-image.png \u001b[90m(1280x720)\u001b[39m\n\n\n\u001b[34m (\u001b[4m\u001b[1mUploading Assets\u001b[22m\u001b[24m)\u001b[39m\n\n - Done Uploading \u001b[90m(1/1)\u001b[39m \u001b[34m/tmp/repo/cypress/screenshots/my-image.png\u001b[39m\n\n\n\u001b[90m (\u001b[4m\u001b[1mAll Done\u001b[22m\u001b[24m)\u001b[39m\n\n", + "status": "passed", + "videos": [], + "wallClockDuration": 16000, + "wallClockEndedAt": null, + "wallClockStartedAt": "2016-12-19T14:12:58.748Z" } ], - "projectId": "3d89712jdha0wef082h98ha-8f98h3", + "loadBalancing": false, + "orgDefault": false, + "orgId": "777", + "orgName": "Acme Developers", + "projectId": "3d897a", + "projectName": "jekyl_blog", + "projectUrl": "http://test-dashboard.cypress.io/#/projects/3d897a", + "specIsolation": false, + "specPattern": null, "status": "running", - "totalDuration": 1424424, - "totalFailures": 0, - "totalPasses": 28, - "totalPending": 4 + "totalDuration": null, + "totalFailed": 2, + "totalPassed": 45, + "totalPending": 6, + "totalSkipped": 1, + "updatedAt": "2016-05-15T02:35:38.687Z", + "runUrl": "http://test-dashboard.cypress.io/#/projects/3d897a/runs/e474ccb9-0352-4ad9-85d3-feeb1e0505d5" }, { - "buildNumber": "1892", - "ciProvider": "CircleCI", - "ciUrl": "https://circleci.com/gh/cypress-io/cypress-core-example/140", - "commitAuthor": "Brian Mann", - "commitBranch": "master", - "commitEmail": "brian@devs.com", - "commitMessage": "fix for smaller screens widths so todos display completely", - "createdAt": "2016-04-21T02:35:12.748Z", - "expectedInstances": 1, + "buildNumber": 1892, + "ci": { + "buildNumber": "140", + "provider": "Circle", + "url": "https://circleci.com/gh/cypress-io/cypress-core-example/140" + }, + "commit": { + "authorEmail": "brian@devs.com", + "authorName": "Brian Mann", + "branch": "master", + "message": "fix for smaller screens widths so todos display completely", + "sha": "abc1234", + "url": "https://github.com/cypress-io/cypress-core-example/commit/abc1234" + }, + "createdAt": "2016-12-19T14:59:59.328Z", + "cypressVersion": "0.18.5", + "completedAt": "2016-05-13T02:52:57.748Z", + "failedTests": [ + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "id": "175e807c-ce85-5f94-938c-e2e30b090633", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r2", + "timings": { + "before each": [ + { + "hookId": "h5a7b63e34b846429cd32b596", + "fnDuration": 6, + "afterFnDuration": 55 + } + ], + "lifecycle": 15 + }, + "title": [ + "Projects", + "lists projects", + "displays all" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + }, + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "failedFromHookId": "h1", + "id": "ef0d934e-a247-5e60-b801-3c5be5aa8796", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r1", + "timings": { + "before each": [ + { + "afterFnDuration": 55, + "fnDuration": 6, + "hookId": "h5a7b63e34b846429cd32b596" + } + ], + "lifecycle": 15 + }, + "title": [ + "Users List", + "lists users", + "displays users roles <select />" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + } + ], "id": "e474ccb9-0352-4ad9-85d3-feeb1e0505d4", "instances": [ { - "browserName": "chrome", - "browserVersion": "43", "createdAt": "2016-05-13T02:35:12.748Z", - "duration": 16, + "claimedAt": "2016-05-13T02:31:12.748Z", + "completedAt": "2016-05-13T02:35:12.748Z", "error": "The tests couldn't run.", - "failures": 0, - "osName": "windows", - "osVersion": "7", - "osVersionFormatted": "7", - "passes": 0, + "failed": 0, + "id": "1dca4de7-eb48-51ee-9bb9-e4c272e6bbc6", + "machineId": "bc98ade3-b54a-5047-922a-aa7cee15a3ae", + "passed": 0, "pending": 0, - "status": "errored" + "platform": { + "browserName": "chrome", + "browserVersion": "43", + "osCpus": [], + "osMemory": { + "free": 985665536, + "total": 63321108480 + }, + "osName": "win32", + "osVersion": "7", + "osVersionFormatted": "7" + }, + "screenshots": [], + "skipped": 1, + "spec": "login_spec.js", + "stdout": "\u001b[90m <select>(\u001b[4m\u001b[1mTests Starting\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[0m\u001b[0m\n\u001b[0m Kitchen Sink\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - assert that <title> is correct\u001b[0m\u001b[31m (358ms)\u001b[0m\n\u001b[0m Querying\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.get() - query DOM elements\u001b[0m\u001b[31m (84ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.contains() - query DOM elements with matching content\u001b[0m\u001b[31m (145ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .within() - query DOM elements within a specific element\u001b[0m\u001b[33m (63ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.root() - query the root DOM element\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[0m Traversal\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .children() - get child DOM elements\u001b[0m\u001b[33m (46ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .closest() - get closest ancestor DOM element\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .eq() - get a DOM element at a specific index\u001b[0m\u001b[33m (61ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .filter() - get DOM elements that match the selector\u001b[0m\u001b[33m (51ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .find() - get descendant DOM elements of the selector\u001b[0m\u001b[31m (82ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .first() - get first DOM element\u001b[0m\u001b[31m (86ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .last() - get last DOM element\u001b[0m\u001b[33m (40ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .next() - get next sibling DOM element\u001b[0m\u001b[33m (42ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextAll() - get all next sibling DOM elements\u001b[0m\u001b[33m (58ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextUntil() - get next sibling DOM elements until next el\u001b[0m\u001b[33m (44ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .not() - remove DOM elements from set of DOM elements\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parent() - get parent DOM element from DOM elements\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parents() - get parent DOM elements from DOM elements\u001b[0m\u001b[33m (52ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parentsUntil() - get parent DOM elements from DOM elements until el\u001b[0m\u001b[33m (54ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prev() - get previous sibling DOM element\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevAll() - get all previous sibling DOM elements\u001b[0m\u001b[33m (48ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevUntil() - get all previous sibling DOM elements until el\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .siblings() - get all sibling DOM elements\u001b[0m\u001b[33m (38ms)\u001b[0m\n\u001b[0m Actions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .type() - type into a DOM element\u001b[0m\u001b[31m (4045ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .focus() - focus on a DOM element\u001b[0m\u001b[31m (118ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .blur() - blur off a DOM element\u001b[0m\u001b[31m (537ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .clear() - clears an input or textarea element\u001b[0m\u001b[31m (825ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .submit() - submit a form\u001b[0m\u001b[31m (423ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .click() - click on a DOM element\u001b[0m\u001b[31m (2555ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .dblclick() - double click on a DOM element\u001b[0m\u001b[31m (120ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.check() - check a checkbox or radio element\u001b[0m\u001b[31m (1968ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .uncheck() - uncheck a checkbox element\u001b[0m\u001b[31m (1637ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .select() - select an option in a <select> element\u001b[0m\u001b[31m (1092ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .scrollIntoView() - scroll an element into view\u001b[0m\u001b[31m (148ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.scrollTo() - scroll the window or element to a position\u001b[0m\u001b[31m (2043ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .trigger() - trigger an event on a DOM element\u001b[0m\u001b[31m (114ms)\u001b[0m\n\u001b[0m Window\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.window() - get the global window object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.document() - get the document object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.title() - get the title\u001b[0m\n\u001b[0m Viewport\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.viewport() - set the viewport size and dimension\u001b[0m\u001b[31m (2771ms)\u001b[0m\n\u001b[0m Location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.hash() - get the current URL hash\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.location() - get window.location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.url() - get the current URL\u001b[0m\n\u001b[0m Navigation\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.go() - go back or forward in the browser's history\u001b[0m\u001b[31m (664ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.reload() - reload the page\u001b[0m\u001b[31m (613ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.visit() - visit a remote url\u001b[0m\u001b[31m (378ms)\u001b[0m\n\u001b[0m Assertions\u001b[0m\n\u001b[0m Implicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - make an assertion about the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .and() - chain multiple assertions together\u001b[0m\n\u001b[0m Explicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - assert shape of an object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - make an assertion about a specified subject\u001b[0m\n\u001b[0m Misc\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .end() - end the command chain\u001b[0m\u001b[31m (269ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.exec() - execute a system command\u001b[0m\u001b[31m (732ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.focused() - get the DOM element that has focus\u001b[0m\u001b[31m (426ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.screenshot() - take a screenshot\u001b[0m\u001b[31m (234ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wrap() - wrap an object\u001b[0m\n\u001b[0m Connectors\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .each() - iterate over an array of elements\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .its() - get properties on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .invoke() - invoke a function on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .spread() - spread an array as individual args to callback function\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .then() - invoke a callback function with the current subject\u001b[0m\u001b[33m (67ms)\u001b[0m\n\u001b[0m Aliasing\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .as() - alias a route or DOM element for later use\u001b[0m\u001b[31m (240ms)\u001b[0m\n\u001b[0m Waiting\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wait() - wait for a specific amount of time\u001b[0m\u001b[31m (4726ms)\u001b[0m\n\u001b[0m Network Requests\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.server() - control behavior of network requests and responses\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.request() - make an XHR request\u001b[0m\u001b[31m (457ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.route() - route responses to matching requests\u001b[0m\u001b[31m (1565ms)\u001b[0m\n\u001b[0m Files\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.fixture() - load a fixture\u001b[0m\u001b[31m (444ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.readFile() - read a files contents\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.writeFile() - write to a file\u001b[0m\u001b[31m (146ms)\u001b[0m\n\u001b[0m Local Storage\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearLocalStorage() - clear all data in local storage\u001b[0m\u001b[31m (402ms)\u001b[0m\n\u001b[0m Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookie() - get a browser cookie\u001b[0m\u001b[31m (168ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookies() - get browser cookies\u001b[0m\u001b[31m (184ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.setCookie() - set a browser cookie\u001b[0m\u001b[33m (39ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookie() - clear a browser cookie\u001b[0m\u001b[31m (189ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookies() - clear browser cookies\u001b[0m\u001b[31m (197ms)\u001b[0m\n\u001b[0m Spies, Stubs, and Clock\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.spy() - wrap a method in a spy\u001b[0m\u001b[31m (472ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.stub() - create a stub and/or replace a function with a stub\u001b[0m\u001b[31m (225ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clock() - control time in the browser\u001b[0m\u001b[31m (383ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.tick() - move time in the browser\u001b[0m\u001b[31m (616ms)\u001b[0m\n\u001b[0m Utilities\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress._.method() - call a lodash method\u001b[0m\u001b[31m (154ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.$(selector) - call a jQuery method\u001b[0m\u001b[31m (188ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.moment() - format or parse dates using a moment method\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Blob.method() - blob utilities and base64 string conversion\u001b[0m\u001b[31m (321ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m new Cypress.Promise(function) - instantiate a bluebird promise\u001b[0m\u001b[31m (1009ms)\u001b[0m\n\u001b[0m Cypress.config()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.config() - get and set configuration options\u001b[0m\u001b[33m (59ms)\u001b[0m\n\u001b[0m Cypress.env()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.env() - get environment variables\u001b[0m\n\u001b[0m Cypress.Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.debug() - enable or disable debugging\u001b[0m\u001b[31m (79ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.preserveOnce() - preserve cookies by key\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.defaults() - set defaults for all cookies\u001b[0m\n\u001b[0m Cypress.dom\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.dom.isHidden() - determine if a DOM element is hidden\u001b[0m\n\u001b[0m Cypress.Server\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Server.defaults() - change default config of server\u001b[0m\n\n\n\u001b[92m \u001b[0m\u001b[32m 90 passing\u001b[0m\u001b[90m (1m)\u001b[0m\n\n\n\u001b[32m (\u001b[4m\u001b[1mTests Finished\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[37m - Tests: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Passes: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Failures: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Pending: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Duration: \u001b[39m\u001b[32m1 minute, 11 seconds\u001b[39m\n\u001b[37m - Screenshots: \u001b[39m\u001b[32m1\u001b[39m\n\u001b[37m - Video Recorded: \u001b[39m\u001b[32mfalse\u001b[39m\n\u001b[37m - Cypress Version: \u001b[39m\u001b[32m1.4.1\u001b[39m\n\n\n\u001b[33m (\u001b[4m\u001b[1mScreenshots\u001b[22m\u001b[24m)\u001b[39m\n\n - /tmp/repo/cypress/screenshots/my-image.png \u001b[90m(1280x720)\u001b[39m\n\n\n\u001b[34m (\u001b[4m\u001b[1mUploading Assets\u001b[22m\u001b[24m)\u001b[39m\n\n - Done Uploading \u001b[90m(1/1)\u001b[39m \u001b[34m/tmp/repo/cypress/screenshots/my-image.png\u001b[39m\n\n\n\u001b[90m (\u001b[4m\u001b[1mAll Done\u001b[22m\u001b[24m)\u001b[39m\n\n", + "status": "errored", + "videos": [], + "wallClockDuration": 16, + "wallClockEndedAt": "2016-05-13T02:35:12.748Z", + "wallClockStartedAt": "2016-05-13T02:31:12.748Z" } ], - "projectId": "4d89712jdha0wef082h98h9098h3", + "loadBalancing": false, + "orgDefault": false, + "orgId": "777", + "orgName": "Acme Developers", + "projectId": "3d897a", + "projectName": "jekyl_blog", + "projectUrl": "http://test-dashboard.cypress.io/#/projects/3d897a", + "specIsolation": false, + "specPattern": null, "status": "failed", - "totalDuration": 16, - "totalFailures": 0, - "totalPasses": 0, - "totalPending": 0 + "totalDuration": 12908, + "totalFailed": 2, + "totalPassed": 45, + "totalPending": 6, + "totalSkipped": 1, + "updatedAt": "2016-05-15T02:35:38.687Z", + "runUrl": "http://test-dashboard.cypress.io/#/projects/3d897a/runs/e474ccb9-0352-4ad9-85d3-feeb1e0505d5" }, { - "buildNumber": "1893", - "ciProvider": "CircleCI", - "ciUrl": "https://circleci.com/gh/cypress-io/cypress-core-example/140", - "commitAuthor": "Julie Pearson", - "commitBranch": "search-todos", - "commitEmail": "julie@devs.com", - "commitMessage": "regex remove whitespace", + "buildNumber": 1893, + "ci": null, + "commit": null, "createdAt": "2016-03-21T02:35:12.748Z", - "expectedInstances": 1, - "id": "e474ccb9-0352-4ad9-85d3-feeb1e0505d3", - "instances": [ - + "cypressVersion": "0.18.5", + "completedAt": "2016-05-13T02:52:57.748Z", + "failedTests": [ + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "id": "175e807c-ce85-5f94-938c-e2e30b090633", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r2", + "timings": { + "before each": [ + { + "hookId": "h5a7b63e34b846429cd32b596", + "fnDuration": 6, + "afterFnDuration": 55 + } + ], + "lifecycle": 15 + }, + "title": [ + "Projects", + "lists projects", + "displays all" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + }, + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "failedFromHookId": "h1", + "id": "ef0d934e-a247-5e60-b801-3c5be5aa8796", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r1", + "timings": { + "before each": [ + { + "afterFnDuration": 55, + "fnDuration": 6, + "hookId": "h5a7b63e34b846429cd32b596" + } + ], + "lifecycle": 15 + }, + "title": [ + "Users List", + "lists users", + "displays users roles <select />" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + } ], - "projectId": "4d89712jdha0wef082h98h9098h3", + "id": "e474ccb9-0352-4ad9-85d3-feeb1e0505d3", + "instances": [], + "loadBalancing": false, + "orgDefault": false, + "orgId": "777", + "orgName": "Acme Developers", + "projectId": "3d897a", + "projectName": "jekyl_blog", + "projectUrl": "http://test-dashboard.cypress.io/#/projects/3d897a", + "specIsolation": false, + "specPattern": null, "status": "errored", - "totalDuration": 1424424, - "totalFailures": 0, - "totalPasses": 28, - "totalPending": 0 + "totalDuration": 1290384, + "totalFailed": 2, + "totalPassed": 45, + "totalPending": 6, + "totalSkipped": 1, + "updatedAt": "2016-05-15T02:35:38.687Z", + "runUrl": "http://test-dashboard.cypress.io/#/projects/3d897a/runs/e474ccb9-0352-4ad9-85d3-feeb1e0505d5" }, { - "buildNumber": "1894", - "ciProvider": "CircleCI", - "ciUrl": "https://circleci.com/gh/cypress-io/cypress-core-example/140", - "commitAuthor": "Julie Pearson", - "commitBranch": "search-todos", - "commitEmail": "julie@devs.com", - "commitMessage": "remove listings from search results on clear", + "buildNumber": 1894, + "ci": { + "buildNumber": "140", + "provider": "Circle", + "url": "https://circleci.com/gh/cypress-io/cypress-core-example/140" + }, + "commit": { + "authorEmail": "julie@devs.com", + "authorName": "Julie Pearson", + "branch": "search-todos", + "message": "remove listings from search results on clear", + "sha": "abc1234", + "url": "https://github.com/cypress-io/cypress-core-example/commit/abc1234" + }, "createdAt": "2015-08-21T02:35:12.748Z", - "expectedInstances": 2, + "cypressVersion": "0.18.5", + "completedAt": "2016-05-13T02:52:57.748Z", + "failedTests": [ + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "id": "175e807c-ce85-5f94-938c-e2e30b090633", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r2", + "timings": { + "before each": [ + { + "hookId": "h5a7b63e34b846429cd32b596", + "fnDuration": 6, + "afterFnDuration": 55 + } + ], + "lifecycle": 15 + }, + "title": [ + "Projects", + "lists projects", + "displays all" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + }, + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "failedFromHookId": "h1", + "id": "ef0d934e-a247-5e60-b801-3c5be5aa8796", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r1", + "timings": { + "before each": [ + { + "afterFnDuration": 55, + "fnDuration": 6, + "hookId": "h5a7b63e34b846429cd32b596" + } + ], + "lifecycle": 15 + }, + "title": [ + "Users List", + "lists users", + "displays users roles <select />" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + } + ], "id": "e474ccb9-0352-4ad9-85d3-feeb1e0505d2", "instances": [ { - "browserName": "chrome", - "browserVersion": "43", "createdAt": "2016-05-13T02:35:12.748Z", - "duration": 1424424, + "claimedAt": "2016-05-13T02:34:12.748Z", + "completedAt": "2016-05-13T02:35:12.748Z", "error": "The tests couldn't run.", - "failures": 0, - "id": "dkwd809-0352-4ad9-85d3-feeb1e0505d5", - "osName": "windows", - "osVersion": "7", - "osVersionFormatted": "7", - "passes": 0, + "failed": 0, + "id": "228c2dee-167d-53b1-98be-da38c5b7bd9a", + "machineId": "bc98ade3-b54a-5047-922a-aa7cee15a3ae", + "passed": 0, "pending": 0, - "status": "errored" + "platform": { + "browserName": "chrome", + "browserVersion": "43", + "osCpus": [], + "osMemory": { + "free": 985665536, + "total": 63321108480 + }, + "osName": "linux", + "osVersion": "14.5", + "osVersionFormatted": "14.5" + }, + "screenshots": [], + "skipped": 1, + "spec": "filters_spec.js", + "stdout": "\u001b[90m <select>(\u001b[4m\u001b[1mTests Starting\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[0m\u001b[0m\n\u001b[0m Kitchen Sink\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - assert that <title> is correct\u001b[0m\u001b[31m (358ms)\u001b[0m\n\u001b[0m Querying\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.get() - query DOM elements\u001b[0m\u001b[31m (84ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.contains() - query DOM elements with matching content\u001b[0m\u001b[31m (145ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .within() - query DOM elements within a specific element\u001b[0m\u001b[33m (63ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.root() - query the root DOM element\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[0m Traversal\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .children() - get child DOM elements\u001b[0m\u001b[33m (46ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .closest() - get closest ancestor DOM element\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .eq() - get a DOM element at a specific index\u001b[0m\u001b[33m (61ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .filter() - get DOM elements that match the selector\u001b[0m\u001b[33m (51ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .find() - get descendant DOM elements of the selector\u001b[0m\u001b[31m (82ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .first() - get first DOM element\u001b[0m\u001b[31m (86ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .last() - get last DOM element\u001b[0m\u001b[33m (40ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .next() - get next sibling DOM element\u001b[0m\u001b[33m (42ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextAll() - get all next sibling DOM elements\u001b[0m\u001b[33m (58ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextUntil() - get next sibling DOM elements until next el\u001b[0m\u001b[33m (44ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .not() - remove DOM elements from set of DOM elements\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parent() - get parent DOM element from DOM elements\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parents() - get parent DOM elements from DOM elements\u001b[0m\u001b[33m (52ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parentsUntil() - get parent DOM elements from DOM elements until el\u001b[0m\u001b[33m (54ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prev() - get previous sibling DOM element\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevAll() - get all previous sibling DOM elements\u001b[0m\u001b[33m (48ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevUntil() - get all previous sibling DOM elements until el\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .siblings() - get all sibling DOM elements\u001b[0m\u001b[33m (38ms)\u001b[0m\n\u001b[0m Actions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .type() - type into a DOM element\u001b[0m\u001b[31m (4045ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .focus() - focus on a DOM element\u001b[0m\u001b[31m (118ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .blur() - blur off a DOM element\u001b[0m\u001b[31m (537ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .clear() - clears an input or textarea element\u001b[0m\u001b[31m (825ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .submit() - submit a form\u001b[0m\u001b[31m (423ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .click() - click on a DOM element\u001b[0m\u001b[31m (2555ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .dblclick() - double click on a DOM element\u001b[0m\u001b[31m (120ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.check() - check a checkbox or radio element\u001b[0m\u001b[31m (1968ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .uncheck() - uncheck a checkbox element\u001b[0m\u001b[31m (1637ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .select() - select an option in a <select> element\u001b[0m\u001b[31m (1092ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .scrollIntoView() - scroll an element into view\u001b[0m\u001b[31m (148ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.scrollTo() - scroll the window or element to a position\u001b[0m\u001b[31m (2043ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .trigger() - trigger an event on a DOM element\u001b[0m\u001b[31m (114ms)\u001b[0m\n\u001b[0m Window\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.window() - get the global window object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.document() - get the document object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.title() - get the title\u001b[0m\n\u001b[0m Viewport\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.viewport() - set the viewport size and dimension\u001b[0m\u001b[31m (2771ms)\u001b[0m\n\u001b[0m Location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.hash() - get the current URL hash\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.location() - get window.location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.url() - get the current URL\u001b[0m\n\u001b[0m Navigation\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.go() - go back or forward in the browser's history\u001b[0m\u001b[31m (664ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.reload() - reload the page\u001b[0m\u001b[31m (613ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.visit() - visit a remote url\u001b[0m\u001b[31m (378ms)\u001b[0m\n\u001b[0m Assertions\u001b[0m\n\u001b[0m Implicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - make an assertion about the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .and() - chain multiple assertions together\u001b[0m\n\u001b[0m Explicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - assert shape of an object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - make an assertion about a specified subject\u001b[0m\n\u001b[0m Misc\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .end() - end the command chain\u001b[0m\u001b[31m (269ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.exec() - execute a system command\u001b[0m\u001b[31m (732ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.focused() - get the DOM element that has focus\u001b[0m\u001b[31m (426ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.screenshot() - take a screenshot\u001b[0m\u001b[31m (234ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wrap() - wrap an object\u001b[0m\n\u001b[0m Connectors\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .each() - iterate over an array of elements\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .its() - get properties on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .invoke() - invoke a function on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .spread() - spread an array as individual args to callback function\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .then() - invoke a callback function with the current subject\u001b[0m\u001b[33m (67ms)\u001b[0m\n\u001b[0m Aliasing\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .as() - alias a route or DOM element for later use\u001b[0m\u001b[31m (240ms)\u001b[0m\n\u001b[0m Waiting\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wait() - wait for a specific amount of time\u001b[0m\u001b[31m (4726ms)\u001b[0m\n\u001b[0m Network Requests\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.server() - control behavior of network requests and responses\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.request() - make an XHR request\u001b[0m\u001b[31m (457ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.route() - route responses to matching requests\u001b[0m\u001b[31m (1565ms)\u001b[0m\n\u001b[0m Files\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.fixture() - load a fixture\u001b[0m\u001b[31m (444ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.readFile() - read a files contents\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.writeFile() - write to a file\u001b[0m\u001b[31m (146ms)\u001b[0m\n\u001b[0m Local Storage\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearLocalStorage() - clear all data in local storage\u001b[0m\u001b[31m (402ms)\u001b[0m\n\u001b[0m Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookie() - get a browser cookie\u001b[0m\u001b[31m (168ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookies() - get browser cookies\u001b[0m\u001b[31m (184ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.setCookie() - set a browser cookie\u001b[0m\u001b[33m (39ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookie() - clear a browser cookie\u001b[0m\u001b[31m (189ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookies() - clear browser cookies\u001b[0m\u001b[31m (197ms)\u001b[0m\n\u001b[0m Spies, Stubs, and Clock\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.spy() - wrap a method in a spy\u001b[0m\u001b[31m (472ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.stub() - create a stub and/or replace a function with a stub\u001b[0m\u001b[31m (225ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clock() - control time in the browser\u001b[0m\u001b[31m (383ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.tick() - move time in the browser\u001b[0m\u001b[31m (616ms)\u001b[0m\n\u001b[0m Utilities\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress._.method() - call a lodash method\u001b[0m\u001b[31m (154ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.$(selector) - call a jQuery method\u001b[0m\u001b[31m (188ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.moment() - format or parse dates using a moment method\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Blob.method() - blob utilities and base64 string conversion\u001b[0m\u001b[31m (321ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m new Cypress.Promise(function) - instantiate a bluebird promise\u001b[0m\u001b[31m (1009ms)\u001b[0m\n\u001b[0m Cypress.config()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.config() - get and set configuration options\u001b[0m\u001b[33m (59ms)\u001b[0m\n\u001b[0m Cypress.env()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.env() - get environment variables\u001b[0m\n\u001b[0m Cypress.Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.debug() - enable or disable debugging\u001b[0m\u001b[31m (79ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.preserveOnce() - preserve cookies by key\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.defaults() - set defaults for all cookies\u001b[0m\n\u001b[0m Cypress.dom\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.dom.isHidden() - determine if a DOM element is hidden\u001b[0m\n\u001b[0m Cypress.Server\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Server.defaults() - change default config of server\u001b[0m\n\n\n\u001b[92m \u001b[0m\u001b[32m 90 passing\u001b[0m\u001b[90m (1m)\u001b[0m\n\n\n\u001b[32m (\u001b[4m\u001b[1mTests Finished\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[37m - Tests: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Passes: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Failures: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Pending: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Duration: \u001b[39m\u001b[32m1 minute, 11 seconds\u001b[39m\n\u001b[37m - Screenshots: \u001b[39m\u001b[32m1\u001b[39m\n\u001b[37m - Video Recorded: \u001b[39m\u001b[32mfalse\u001b[39m\n\u001b[37m - Cypress Version: \u001b[39m\u001b[32m1.4.1\u001b[39m\n\n\n\u001b[33m (\u001b[4m\u001b[1mScreenshots\u001b[22m\u001b[24m)\u001b[39m\n\n - /tmp/repo/cypress/screenshots/my-image.png \u001b[90m(1280x720)\u001b[39m\n\n\n\u001b[34m (\u001b[4m\u001b[1mUploading Assets\u001b[22m\u001b[24m)\u001b[39m\n\n - Done Uploading \u001b[90m(1/1)\u001b[39m \u001b[34m/tmp/repo/cypress/screenshots/my-image.png\u001b[39m\n\n\n\u001b[90m (\u001b[4m\u001b[1mAll Done\u001b[22m\u001b[24m)\u001b[39m\n\n", + "status": "errored", + "videos": [], + "wallClockDuration": 1424424, + "wallClockEndedAt": "2016-05-13T02:35:12.748Z", + "wallClockStartedAt": "2016-05-13T02:34:12.748Z" }, { - "browserName": "safari", - "browserVersion": "13.5", "createdAt": "2016-05-13T02:35:12.748Z", - "duration": 1424424, + "claimedAt": "2016-05-13T02:34:12.748Z", + "completedAt": "2016-05-13T02:35:12.748Z", "error": null, - "failures": 0, - "id": "as9d0809-0352-4ad9-85d3-feeb1e0505d5", - "osName": "darwin", - "osVersion": "10", - "osVersionFormatted": "OSX Mountain Lion", - "passes": 0, + "failed": 0, + "id": "71272f43-0891-588a-a822-5ad143985fc3", + "machineId": "bc98ade3-b54a-5047-922a-aa7cee15a3ae", + "passed": 0, "pending": 0, - "status": "running" + "platform": { + "browserName": "safari", + "browserVersion": "13.5", + "osCpus": [], + "osMemory": { + "free": 985665536, + "total": 63321108480 + }, + "osName": "darwin", + "osVersion": "10", + "osVersionFormatted": "OSX Mountain Lion" + }, + "screenshots": [], + "skipped": 1, + "spec": "search_spec.js", + "stdout": "\u001b[90m <select>(\u001b[4m\u001b[1mTests Starting\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[0m\u001b[0m\n\u001b[0m Kitchen Sink\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - assert that <title> is correct\u001b[0m\u001b[31m (358ms)\u001b[0m\n\u001b[0m Querying\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.get() - query DOM elements\u001b[0m\u001b[31m (84ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.contains() - query DOM elements with matching content\u001b[0m\u001b[31m (145ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .within() - query DOM elements within a specific element\u001b[0m\u001b[33m (63ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.root() - query the root DOM element\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[0m Traversal\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .children() - get child DOM elements\u001b[0m\u001b[33m (46ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .closest() - get closest ancestor DOM element\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .eq() - get a DOM element at a specific index\u001b[0m\u001b[33m (61ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .filter() - get DOM elements that match the selector\u001b[0m\u001b[33m (51ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .find() - get descendant DOM elements of the selector\u001b[0m\u001b[31m (82ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .first() - get first DOM element\u001b[0m\u001b[31m (86ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .last() - get last DOM element\u001b[0m\u001b[33m (40ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .next() - get next sibling DOM element\u001b[0m\u001b[33m (42ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextAll() - get all next sibling DOM elements\u001b[0m\u001b[33m (58ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextUntil() - get next sibling DOM elements until next el\u001b[0m\u001b[33m (44ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .not() - remove DOM elements from set of DOM elements\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parent() - get parent DOM element from DOM elements\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parents() - get parent DOM elements from DOM elements\u001b[0m\u001b[33m (52ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parentsUntil() - get parent DOM elements from DOM elements until el\u001b[0m\u001b[33m (54ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prev() - get previous sibling DOM element\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevAll() - get all previous sibling DOM elements\u001b[0m\u001b[33m (48ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevUntil() - get all previous sibling DOM elements until el\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .siblings() - get all sibling DOM elements\u001b[0m\u001b[33m (38ms)\u001b[0m\n\u001b[0m Actions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .type() - type into a DOM element\u001b[0m\u001b[31m (4045ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .focus() - focus on a DOM element\u001b[0m\u001b[31m (118ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .blur() - blur off a DOM element\u001b[0m\u001b[31m (537ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .clear() - clears an input or textarea element\u001b[0m\u001b[31m (825ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .submit() - submit a form\u001b[0m\u001b[31m (423ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .click() - click on a DOM element\u001b[0m\u001b[31m (2555ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .dblclick() - double click on a DOM element\u001b[0m\u001b[31m (120ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.check() - check a checkbox or radio element\u001b[0m\u001b[31m (1968ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .uncheck() - uncheck a checkbox element\u001b[0m\u001b[31m (1637ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .select() - select an option in a <select> element\u001b[0m\u001b[31m (1092ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .scrollIntoView() - scroll an element into view\u001b[0m\u001b[31m (148ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.scrollTo() - scroll the window or element to a position\u001b[0m\u001b[31m (2043ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .trigger() - trigger an event on a DOM element\u001b[0m\u001b[31m (114ms)\u001b[0m\n\u001b[0m Window\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.window() - get the global window object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.document() - get the document object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.title() - get the title\u001b[0m\n\u001b[0m Viewport\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.viewport() - set the viewport size and dimension\u001b[0m\u001b[31m (2771ms)\u001b[0m\n\u001b[0m Location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.hash() - get the current URL hash\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.location() - get window.location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.url() - get the current URL\u001b[0m\n\u001b[0m Navigation\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.go() - go back or forward in the browser's history\u001b[0m\u001b[31m (664ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.reload() - reload the page\u001b[0m\u001b[31m (613ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.visit() - visit a remote url\u001b[0m\u001b[31m (378ms)\u001b[0m\n\u001b[0m Assertions\u001b[0m\n\u001b[0m Implicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - make an assertion about the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .and() - chain multiple assertions together\u001b[0m\n\u001b[0m Explicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - assert shape of an object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - make an assertion about a specified subject\u001b[0m\n\u001b[0m Misc\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .end() - end the command chain\u001b[0m\u001b[31m (269ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.exec() - execute a system command\u001b[0m\u001b[31m (732ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.focused() - get the DOM element that has focus\u001b[0m\u001b[31m (426ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.screenshot() - take a screenshot\u001b[0m\u001b[31m (234ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wrap() - wrap an object\u001b[0m\n\u001b[0m Connectors\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .each() - iterate over an array of elements\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .its() - get properties on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .invoke() - invoke a function on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .spread() - spread an array as individual args to callback function\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .then() - invoke a callback function with the current subject\u001b[0m\u001b[33m (67ms)\u001b[0m\n\u001b[0m Aliasing\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .as() - alias a route or DOM element for later use\u001b[0m\u001b[31m (240ms)\u001b[0m\n\u001b[0m Waiting\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wait() - wait for a specific amount of time\u001b[0m\u001b[31m (4726ms)\u001b[0m\n\u001b[0m Network Requests\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.server() - control behavior of network requests and responses\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.request() - make an XHR request\u001b[0m\u001b[31m (457ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.route() - route responses to matching requests\u001b[0m\u001b[31m (1565ms)\u001b[0m\n\u001b[0m Files\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.fixture() - load a fixture\u001b[0m\u001b[31m (444ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.readFile() - read a files contents\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.writeFile() - write to a file\u001b[0m\u001b[31m (146ms)\u001b[0m\n\u001b[0m Local Storage\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearLocalStorage() - clear all data in local storage\u001b[0m\u001b[31m (402ms)\u001b[0m\n\u001b[0m Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookie() - get a browser cookie\u001b[0m\u001b[31m (168ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookies() - get browser cookies\u001b[0m\u001b[31m (184ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.setCookie() - set a browser cookie\u001b[0m\u001b[33m (39ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookie() - clear a browser cookie\u001b[0m\u001b[31m (189ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookies() - clear browser cookies\u001b[0m\u001b[31m (197ms)\u001b[0m\n\u001b[0m Spies, Stubs, and Clock\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.spy() - wrap a method in a spy\u001b[0m\u001b[31m (472ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.stub() - create a stub and/or replace a function with a stub\u001b[0m\u001b[31m (225ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clock() - control time in the browser\u001b[0m\u001b[31m (383ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.tick() - move time in the browser\u001b[0m\u001b[31m (616ms)\u001b[0m\n\u001b[0m Utilities\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress._.method() - call a lodash method\u001b[0m\u001b[31m (154ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.$(selector) - call a jQuery method\u001b[0m\u001b[31m (188ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.moment() - format or parse dates using a moment method\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Blob.method() - blob utilities and base64 string conversion\u001b[0m\u001b[31m (321ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m new Cypress.Promise(function) - instantiate a bluebird promise\u001b[0m\u001b[31m (1009ms)\u001b[0m\n\u001b[0m Cypress.config()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.config() - get and set configuration options\u001b[0m\u001b[33m (59ms)\u001b[0m\n\u001b[0m Cypress.env()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.env() - get environment variables\u001b[0m\n\u001b[0m Cypress.Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.debug() - enable or disable debugging\u001b[0m\u001b[31m (79ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.preserveOnce() - preserve cookies by key\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.defaults() - set defaults for all cookies\u001b[0m\n\u001b[0m Cypress.dom\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.dom.isHidden() - determine if a DOM element is hidden\u001b[0m\n\u001b[0m Cypress.Server\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Server.defaults() - change default config of server\u001b[0m\n\n\n\u001b[92m \u001b[0m\u001b[32m 90 passing\u001b[0m\u001b[90m (1m)\u001b[0m\n\n\n\u001b[32m (\u001b[4m\u001b[1mTests Finished\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[37m - Tests: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Passes: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Failures: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Pending: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Duration: \u001b[39m\u001b[32m1 minute, 11 seconds\u001b[39m\n\u001b[37m - Screenshots: \u001b[39m\u001b[32m1\u001b[39m\n\u001b[37m - Video Recorded: \u001b[39m\u001b[32mfalse\u001b[39m\n\u001b[37m - Cypress Version: \u001b[39m\u001b[32m1.4.1\u001b[39m\n\n\n\u001b[33m (\u001b[4m\u001b[1mScreenshots\u001b[22m\u001b[24m)\u001b[39m\n\n - /tmp/repo/cypress/screenshots/my-image.png \u001b[90m(1280x720)\u001b[39m\n\n\n\u001b[34m (\u001b[4m\u001b[1mUploading Assets\u001b[22m\u001b[24m)\u001b[39m\n\n - Done Uploading \u001b[90m(1/1)\u001b[39m \u001b[34m/tmp/repo/cypress/screenshots/my-image.png\u001b[39m\n\n\n\u001b[90m (\u001b[4m\u001b[1mAll Done\u001b[22m\u001b[24m)\u001b[39m\n\n", + "status": "running", + "videos": [], + "wallClockDuration": 1424424, + "wallClockEndedAt": "2016-05-13T02:35:12.748Z", + "wallClockStartedAt": "2016-05-13T02:34:12.748Z" } ], - "projectId": "2d89712jdha0wef082h98h9098h3", + "loadBalancing": false, + "orgDefault": false, + "orgId": "777", + "orgName": "Acme Developers", + "projectId": "3d897a", + "projectName": "jekyl_blog", + "projectUrl": "http://test-dashboard.cypress.io/#/projects/3d897a", + "specIsolation": false, + "specPattern": null, "status": "passed", - "totalDuration": 1424424, - "totalFailures": 0, - "totalPasses": 28, - "totalPending": 2 + "totalDuration": 9018203, + "totalFailed": 0, + "totalPassed": 45, + "totalPending": 6, + "totalSkipped": 1, + "updatedAt": "2016-05-15T02:35:38.687Z", + "runUrl": "http://test-dashboard.cypress.io/#/projects/3d897a/runs/e474ccb9-0352-4ad9-85d3-feeb1e0505d5" + }, + { + "buildNumber": 1891, + "ci": { + "buildNumber": "140", + "provider": "CircleCI", + "url": "https://circleci.com/gh/cypress-io/cypress-core-example/140" + }, + "commit": { + "authorEmail": "julie@devs.com", + "authorName": "Julie Pearson", + "branch": "search-todos", + "message": "remove listings from search results on clear", + "sha": "abc1234", + "url": "https://github.com/cypress-io/cypress-core-example/commit/abc1234" + }, + "createdAt": "2016-12-19T14:02:57.328Z", + "cypressVersion": "0.18.5", + "completedAt": null, + "failedTests": [ + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "id": "175e807c-ce85-5f94-938c-e2e30b090633", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r2", + "timings": { + "before each": [ + { + "hookId": "h5a7b63e34b846429cd32b596", + "fnDuration": 6, + "afterFnDuration": 55 + } + ], + "lifecycle": 15 + }, + "title": [ + "Projects", + "lists projects", + "displays all" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + }, + { + "body": "function () {}", + "error": "Timed out retrying: cy.click() failed because this element is being covered by another element:", + "failedFromHookId": "h1", + "id": "ef0d934e-a247-5e60-b801-3c5be5aa8796", + "instanceId": "d1609552-31b5-50c1-b307-c27c9553ccb8", + "stack": "Error: Uncaught ReferenceError:\n at Object.$Cypress.Utils._.cypressErr (http://localhost:2020/__cypress/static/js/cypress.js:4678:15)\n at Object.$Cypress.Utils._.throwErr (http://localhost:2020/__cypress/static/js/cypress.js:4642:22)\n at Object.$Cypress.Utils._.throwErrByPath (http://localhost:2020/__cypress/static/js/cypress.js:4670:21)", + "state": "failed", + "testId": "r1", + "timings": { + "before each": [ + { + "afterFnDuration": 55, + "fnDuration": 6, + "hookId": "h5a7b63e34b846429cd32b596" + } + ], + "lifecycle": 15 + }, + "title": [ + "Users List", + "lists users", + "displays users roles <select />" + ], + "videoTimestamp": 1000, + "wallClockDuration": 1234, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z" + } + ], + "id": "5c0ef8fa-c61c-5a63-a450-2598dada80f7", + "instances": [ + { + "createdAt": "2016-12-19T14:22:57.748Z", + "claimedAt": "2016-12-19T14:12:57.748Z", + "completedAt": null, + "error": null, + "failed": 0, + "id": "923886c6-dbe4-5a7e-988c-97dbbf9d17ff", + "machineId": "bc98ade3-b54a-5047-922a-aa7cee15a3ae", + "passed": 28, + "pending": 4, + "platform": { + "browserName": "chrome", + "browserVersion": "43", + "osCpus": [], + "osMemory": { + "free": 985665536, + "total": 63321108480 + }, + "osName": "win32", + "osVersion": "7", + "osVersionFormatted": "7" + }, + "screenshots": [], + "skipped": 1, + "spec": "users_list_spec.js", + "stdout": "\u001b[90m <select>(\u001b[4m\u001b[1mTests Starting\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[0m\u001b[0m\n\u001b[0m Kitchen Sink\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - assert that <title> is correct\u001b[0m\u001b[31m (358ms)\u001b[0m\n\u001b[0m Querying\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.get() - query DOM elements\u001b[0m\u001b[31m (84ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.contains() - query DOM elements with matching content\u001b[0m\u001b[31m (145ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .within() - query DOM elements within a specific element\u001b[0m\u001b[33m (63ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.root() - query the root DOM element\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[0m Traversal\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .children() - get child DOM elements\u001b[0m\u001b[33m (46ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .closest() - get closest ancestor DOM element\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .eq() - get a DOM element at a specific index\u001b[0m\u001b[33m (61ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .filter() - get DOM elements that match the selector\u001b[0m\u001b[33m (51ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .find() - get descendant DOM elements of the selector\u001b[0m\u001b[31m (82ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .first() - get first DOM element\u001b[0m\u001b[31m (86ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .last() - get last DOM element\u001b[0m\u001b[33m (40ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .next() - get next sibling DOM element\u001b[0m\u001b[33m (42ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextAll() - get all next sibling DOM elements\u001b[0m\u001b[33m (58ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .nextUntil() - get next sibling DOM elements until next el\u001b[0m\u001b[33m (44ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .not() - remove DOM elements from set of DOM elements\u001b[0m\u001b[33m (43ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parent() - get parent DOM element from DOM elements\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parents() - get parent DOM elements from DOM elements\u001b[0m\u001b[33m (52ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .parentsUntil() - get parent DOM elements from DOM elements until el\u001b[0m\u001b[33m (54ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prev() - get previous sibling DOM element\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevAll() - get all previous sibling DOM elements\u001b[0m\u001b[33m (48ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .prevUntil() - get all previous sibling DOM elements until el\u001b[0m\u001b[33m (49ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .siblings() - get all sibling DOM elements\u001b[0m\u001b[33m (38ms)\u001b[0m\n\u001b[0m Actions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .type() - type into a DOM element\u001b[0m\u001b[31m (4045ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .focus() - focus on a DOM element\u001b[0m\u001b[31m (118ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .blur() - blur off a DOM element\u001b[0m\u001b[31m (537ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .clear() - clears an input or textarea element\u001b[0m\u001b[31m (825ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .submit() - submit a form\u001b[0m\u001b[31m (423ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .click() - click on a DOM element\u001b[0m\u001b[31m (2555ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .dblclick() - double click on a DOM element\u001b[0m\u001b[31m (120ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.check() - check a checkbox or radio element\u001b[0m\u001b[31m (1968ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .uncheck() - uncheck a checkbox element\u001b[0m\u001b[31m (1637ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .select() - select an option in a <select> element\u001b[0m\u001b[31m (1092ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .scrollIntoView() - scroll an element into view\u001b[0m\u001b[31m (148ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.scrollTo() - scroll the window or element to a position\u001b[0m\u001b[31m (2043ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .trigger() - trigger an event on a DOM element\u001b[0m\u001b[31m (114ms)\u001b[0m\n\u001b[0m Window\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.window() - get the global window object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.document() - get the document object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.title() - get the title\u001b[0m\n\u001b[0m Viewport\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.viewport() - set the viewport size and dimension\u001b[0m\u001b[31m (2771ms)\u001b[0m\n\u001b[0m Location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.hash() - get the current URL hash\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.location() - get window.location\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.url() - get the current URL\u001b[0m\n\u001b[0m Navigation\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.go() - go back or forward in the browser's history\u001b[0m\u001b[31m (664ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.reload() - reload the page\u001b[0m\u001b[31m (613ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.visit() - visit a remote url\u001b[0m\u001b[31m (378ms)\u001b[0m\n\u001b[0m Assertions\u001b[0m\n\u001b[0m Implicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .should() - make an assertion about the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .and() - chain multiple assertions together\u001b[0m\n\u001b[0m Explicit Assertions\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - assert shape of an object\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m expect - make an assertion about a specified subject\u001b[0m\n\u001b[0m Misc\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .end() - end the command chain\u001b[0m\u001b[31m (269ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.exec() - execute a system command\u001b[0m\u001b[31m (732ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.focused() - get the DOM element that has focus\u001b[0m\u001b[31m (426ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.screenshot() - take a screenshot\u001b[0m\u001b[31m (234ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wrap() - wrap an object\u001b[0m\n\u001b[0m Connectors\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .each() - iterate over an array of elements\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .its() - get properties on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .invoke() - invoke a function on the current subject\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .spread() - spread an array as individual args to callback function\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .then() - invoke a callback function with the current subject\u001b[0m\u001b[33m (67ms)\u001b[0m\n\u001b[0m Aliasing\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m .as() - alias a route or DOM element for later use\u001b[0m\u001b[31m (240ms)\u001b[0m\n\u001b[0m Waiting\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.wait() - wait for a specific amount of time\u001b[0m\u001b[31m (4726ms)\u001b[0m\n\u001b[0m Network Requests\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.server() - control behavior of network requests and responses\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.request() - make an XHR request\u001b[0m\u001b[31m (457ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.route() - route responses to matching requests\u001b[0m\u001b[31m (1565ms)\u001b[0m\n\u001b[0m Files\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.fixture() - load a fixture\u001b[0m\u001b[31m (444ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.readFile() - read a files contents\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.writeFile() - write to a file\u001b[0m\u001b[31m (146ms)\u001b[0m\n\u001b[0m Local Storage\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearLocalStorage() - clear all data in local storage\u001b[0m\u001b[31m (402ms)\u001b[0m\n\u001b[0m Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookie() - get a browser cookie\u001b[0m\u001b[31m (168ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.getCookies() - get browser cookies\u001b[0m\u001b[31m (184ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.setCookie() - set a browser cookie\u001b[0m\u001b[33m (39ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookie() - clear a browser cookie\u001b[0m\u001b[31m (189ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clearCookies() - clear browser cookies\u001b[0m\u001b[31m (197ms)\u001b[0m\n\u001b[0m Spies, Stubs, and Clock\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.spy() - wrap a method in a spy\u001b[0m\u001b[31m (472ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.stub() - create a stub and/or replace a function with a stub\u001b[0m\u001b[31m (225ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.clock() - control time in the browser\u001b[0m\u001b[31m (383ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m cy.tick() - move time in the browser\u001b[0m\u001b[31m (616ms)\u001b[0m\n\u001b[0m Utilities\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress._.method() - call a lodash method\u001b[0m\u001b[31m (154ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.$(selector) - call a jQuery method\u001b[0m\u001b[31m (188ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.moment() - format or parse dates using a moment method\u001b[0m\u001b[33m (41ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Blob.method() - blob utilities and base64 string conversion\u001b[0m\u001b[31m (321ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m new Cypress.Promise(function) - instantiate a bluebird promise\u001b[0m\u001b[31m (1009ms)\u001b[0m\n\u001b[0m Cypress.config()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.config() - get and set configuration options\u001b[0m\u001b[33m (59ms)\u001b[0m\n\u001b[0m Cypress.env()\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.env() - get environment variables\u001b[0m\n\u001b[0m Cypress.Cookies\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.debug() - enable or disable debugging\u001b[0m\u001b[31m (79ms)\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.preserveOnce() - preserve cookies by key\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Cookies.defaults() - set defaults for all cookies\u001b[0m\n\u001b[0m Cypress.dom\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.dom.isHidden() - determine if a DOM element is hidden\u001b[0m\n\u001b[0m Cypress.Server\u001b[0m\n\u001b[2K\u001b[0G \u001b[32m ✓\u001b[0m\u001b[90m Cypress.Server.defaults() - change default config of server\u001b[0m\n\n\n\u001b[92m \u001b[0m\u001b[32m 90 passing\u001b[0m\u001b[90m (1m)\u001b[0m\n\n\n\u001b[32m (\u001b[4m\u001b[1mTests Finished\u001b[22m\u001b[24m)\u001b[39m\n\n\u001b[37m - Tests: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Passes: \u001b[39m\u001b[32m90\u001b[39m\n\u001b[37m - Failures: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Pending: \u001b[39m\u001b[32m0\u001b[39m\n\u001b[37m - Duration: \u001b[39m\u001b[32m1 minute, 11 seconds\u001b[39m\n\u001b[37m - Screenshots: \u001b[39m\u001b[32m1\u001b[39m\n\u001b[37m - Video Recorded: \u001b[39m\u001b[32mfalse\u001b[39m\n\u001b[37m - Cypress Version: \u001b[39m\u001b[32m1.4.1\u001b[39m\n\n\n\u001b[33m (\u001b[4m\u001b[1mScreenshots\u001b[22m\u001b[24m)\u001b[39m\n\n - /tmp/repo/cypress/screenshots/my-image.png \u001b[90m(1280x720)\u001b[39m\n\n\n\u001b[34m (\u001b[4m\u001b[1mUploading Assets\u001b[22m\u001b[24m)\u001b[39m\n\n - Done Uploading \u001b[90m(1/1)\u001b[39m \u001b[34m/tmp/repo/cypress/screenshots/my-image.png\u001b[39m\n\n\n\u001b[90m (\u001b[4m\u001b[1mAll Done\u001b[22m\u001b[24m)\u001b[39m\n\n", + "status": "passed", + "videos": [], + "wallClockDuration": 16000, + "wallClockEndedAt": null, + "wallClockStartedAt": "2016-12-19T14:12:58.748Z" + } + ], + "loadBalancing": false, + "orgDefault": false, + "orgId": "777", + "orgName": "Acme Developers", + "projectId": "3d897a", + "projectName": "jekyl_blog", + "projectUrl": "http://test-dashboard.cypress.io/#/projects/3d897a", + "specIsolation": false, + "specPattern": null, + "status": "running", + "totalDuration": null, + "totalFailed": 2, + "totalPassed": 45, + "totalPending": 6, + "totalSkipped": 1, + "updatedAt": "2016-05-15T02:35:38.687Z", + "runUrl": "http://test-dashboard.cypress.io/#/projects/3d897a/runs/e474ccb9-0352-4ad9-85d3-feeb1e0505d5" } ] diff --git a/packages/desktop-gui/cypress/integration/fixtures_spec.coffee b/packages/desktop-gui/cypress/integration/fixtures_spec.coffee new file mode 100644 index 000000000000..c87094e9de19 --- /dev/null +++ b/packages/desktop-gui/cypress/integration/fixtures_spec.coffee @@ -0,0 +1,9 @@ +jsonSchemas = require("@cypress/json-schemas") + +{ assertSchema } = jsonSchemas + +describe "api object schemas matches fixture: ", -> + it "runs", -> + cy.fixture("runs.json").each assertSchema("getRunResponse", "2.0.0", { + substitutions: ["orgId"], omit: { object: true, example: true } + }) diff --git a/packages/desktop-gui/cypress/integration/project_mode_spec.coffee b/packages/desktop-gui/cypress/integration/project_mode_spec.coffee index 54b431c99703..b2ec00f79888 100644 --- a/packages/desktop-gui/cypress/integration/project_mode_spec.coffee +++ b/packages/desktop-gui/cypress/integration/project_mode_spec.coffee @@ -9,7 +9,7 @@ describe "Project Mode", -> cy.stub(@ipc, "onMenuClicked") cy.stub(@ipc, "onFocusTests") - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "updaterCheck").resolves(false) cy.stub(@ipc, "openProject").resolves(@config) cy.stub(@ipc, "getSpecs").yields(null, @specs) diff --git a/packages/desktop-gui/cypress/integration/project_nav_spec.coffee b/packages/desktop-gui/cypress/integration/project_nav_spec.coffee index b73ae4c5b92c..2bf803f87bdc 100644 --- a/packages/desktop-gui/cypress/integration/project_nav_spec.coffee +++ b/packages/desktop-gui/cypress/integration/project_nav_spec.coffee @@ -8,7 +8,7 @@ describe "Project Nav", -> cy.visitIndex().then (win) -> { start, @ipc } = win.App - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "updaterCheck").resolves(false) cy.stub(@ipc, "getCurrentUser").resolves(@user) cy.stub(@ipc, "getRuns").resolves(@runs) @@ -33,14 +33,12 @@ describe "Project Nav", -> @openProject.resolve(@config) it "displays projects nav", -> - cy - .get(".empty").should("not.be.visible") - .get(".navbar-default") + cy.get(".empty").should("not.be.visible") + cy.get(".navbar-default") it "displays 'Tests' nav as active", -> - cy - .get(".navbar-default").contains("a", "Tests") - .should("have.class", "active") + cy.get(".navbar-default").contains("a", "Tests") + .should("have.class", "active") describe "when project loads", -> beforeEach -> @@ -51,31 +49,26 @@ describe "Project Nav", -> describe "runs page", -> beforeEach -> - cy - .fixture("runs").as("runs") - .get(".navbar-default") - .contains("a", "Runs").as("runsNav").click() + cy.fixture("runs").as("runs") + cy.get(".navbar-default") + .contains("a", "Runs").as("runsNav").click() it "highlights runs on click", -> - cy - .get("@runsNav") - .should("have.class", "active") + cy.get("@runsNav") + .should("have.class", "active") it "displays runs page", -> - cy - .get(".runs-container li") - .should("have.length", 4) + cy.get(".runs-container li") + .should("have.length", @runs.length) describe "settings page", -> beforeEach -> - cy - .get(".navbar-default") - .contains("a", "Settings").as("settingsNav").click() + cy.get(".navbar-default") + .contains("a", "Settings").as("settingsNav").click() it "highlights config on click", -> - cy - .get("@settingsNav") - .should("have.class", "active") + cy.get("@settingsNav") + .should("have.class", "active") it "displays settings page", -> cy.contains("Configuration") @@ -87,45 +80,38 @@ describe "Project Nav", -> context "normal browser list behavior", -> it "lists browsers", -> - cy - .get(".browsers-list").parent() + cy.get(".browsers-list").parent() .find(".dropdown-menu").first().find("li").should("have.length", 2) .should ($li) -> expect($li.first()).to.contain("Chromium") expect($li.last()).to.contain("Canary") it "does not display stop button", -> - cy - .get(".close-browser").should("not.exist") + cy.get(".close-browser").should("not.exist") describe "default browser", -> it "displays default browser name in chosen", -> - cy - .get(".browsers-list>a").first() - .should("contain", "Chrome") + cy.get(".browsers-list>a").first() + .should("contain", "Chrome") it "displays default browser icon in chosen", -> - cy - .get(".browsers-list>a").first() - .find(".fa-chrome") + cy.get(".browsers-list>a").first() + .find(".fa-chrome") context "switch browser", -> beforeEach -> - cy - .get(".browsers-list>a").first().click() - .get(".browsers-list").find(".dropdown-menu") - .contains("Chromium").click() + cy.get(".browsers-list>a").first().click() + cy.get(".browsers-list").find(".dropdown-menu") + .contains("Chromium").click() afterEach -> cy.clearLocalStorage() it "switches text in button on switching browser", -> - cy - .get(".browsers-list>a").first().contains("Chromium") + cy.get(".browsers-list>a").first().contains("Chromium") it "swaps the chosen browser into the dropdown", -> - cy - .get(".browsers-list").find(".dropdown-menu") + cy.get(".browsers-list").find(".dropdown-menu") .find("li").should("have.length", 2) .should ($li) -> expect($li.first()).to.contain("Chrome") @@ -139,14 +125,12 @@ describe "Project Nav", -> cy.contains(".file", "app_spec").click() it "displays browser icon as spinner", -> - cy - .get(".browsers-list>a").first().find("i") - .should("have.class", "fa fa-refresh fa-spin") + cy.get(".browsers-list>a").first().find("i") + .should("have.class", "fa fa-refresh fa-spin") it "disables browser dropdown", -> - cy - .get(".browsers-list>a").first() - .and("have.class", "disabled") + cy.get(".browsers-list>a").first() + .should("have.class", "disabled") context "browser opened after choosing spec", -> beforeEach -> @@ -154,18 +138,15 @@ describe "Project Nav", -> cy.contains(".file", "app_spec").click() it "displays browser icon as opened", -> - cy - .get(".browsers-list>a").first().find("i") - .should("have.class", "fa fa-check-circle-o") + cy.get(".browsers-list>a").first().find("i") + .should("have.class", "fa fa-check-circle-o") it "disables browser dropdown", -> - cy - .get(".browsers-list>a").first() - .should("have.class", "disabled") + cy.get(".browsers-list>a").first() + .should("have.class", "disabled") it "displays stop browser button", -> - cy - .get(".close-browser").should("be.visible") + cy.get(".close-browser").should("be.visible") describe "stop browser", -> beforeEach -> @@ -178,14 +159,12 @@ describe "Project Nav", -> cy.get(".close-browser").should("not.exist") it "re-enables browser dropdown", -> - cy - .get(".browsers-list>a").first() - .should("not.have.class", "disabled") + cy.get(".browsers-list>a").first() + .should("not.have.class", "disabled") it "displays default browser icon", -> - cy - .get(".browsers-list>a").first() - .find(".fa-chrome") + cy.get(".browsers-list>a").first() + .find(".fa-chrome") describe "browser is closed manually", -> beforeEach -> @@ -211,14 +190,12 @@ describe "Project Nav", -> cy.clearLocalStorage() it "displays local storage browser name in chosen", -> - cy - .get(".browsers-list>a").first() - .should("contain", "Chromium") + cy.get(".browsers-list>a").first() + .should("contain", "Chromium") it "displays local storage browser icon in chosen", -> - cy - .get(".browsers-list>a").first() - .find(".fa-chrome") + cy.get(".browsers-list>a").first() + .find(".fa-chrome") describe "when browser saved in local storage no longer exists", -> beforeEach -> @@ -226,9 +203,8 @@ describe "Project Nav", -> @openProject.resolve(@config) it "defaults to first browser", -> - cy - .get(".browsers-list>a").first() - .should("contain", "Chrome") + cy.get(".browsers-list>a").first() + .should("contain", "Chrome") describe "only one browser available", -> beforeEach -> @@ -243,9 +219,8 @@ describe "Project Nav", -> @openProject.resolve(@config) it "displays no dropdown btn", -> - cy - .get(".browsers-list") - .find(".dropdown-toggle").should("not.be.visible") + cy.get(".browsers-list") + .find(".dropdown-toggle").should("not.be.visible") describe "browser with info", -> beforeEach -> @@ -261,9 +236,8 @@ describe "Project Nav", -> @openProject.resolve(@config) it "shows info icon with tooltip", -> - cy - .get(".browsers .fa-info-circle") + cy.get(".browsers .fa-info-circle") .then ($el) -> $el[0].dispatchEvent(new Event("mouseover", {bubbles: true})) - .get(".cy-tooltip") + cy.get(".cy-tooltip") .should("contain", @info) diff --git a/packages/desktop-gui/cypress/integration/project_spec.coffee b/packages/desktop-gui/cypress/integration/project_spec.coffee index 8d3ff4dfe6ff..7aec8d18f9ba 100644 --- a/packages/desktop-gui/cypress/integration/project_spec.coffee +++ b/packages/desktop-gui/cypress/integration/project_spec.coffee @@ -8,7 +8,7 @@ describe "Project", -> cy.visitIndex().then (win) => { @start, @ipc } = win.App - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "getCurrentUser").resolves(@user) cy.stub(@ipc, "openProject").resolves(@config) cy.stub(@ipc, "getSpecs").yields(null, @specs) diff --git a/packages/desktop-gui/cypress/integration/runs_list_spec.coffee b/packages/desktop-gui/cypress/integration/runs_list_spec.coffee index 7e5044514cc4..d8bc73b4e91c 100644 --- a/packages/desktop-gui/cypress/integration/runs_list_spec.coffee +++ b/packages/desktop-gui/cypress/integration/runs_list_spec.coffee @@ -1,3 +1,5 @@ +moment = require("moment") + describe "Runs List", -> beforeEach -> cy.fixture("user").as("user") @@ -8,9 +10,8 @@ describe "Runs List", -> cy.fixture("organizations").as("orgs") @goToRuns = -> - cy - .get(".navbar-default a") - .contains("Runs").click() + cy.get(".navbar-default a") + .contains("Runs").click() @validCiProject = { id: "project-id-123" @@ -21,7 +22,7 @@ describe "Runs List", -> cy.visitIndex().then (win) -> { start, @ipc } = win.App - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "updaterCheck").resolves(false) cy.stub(@ipc, "closeBrowser").resolves(null) cy.stub(@ipc, "getSpecs").yields(null, @specs) @@ -56,12 +57,13 @@ describe "Runs List", -> @goToRuns() it "highlights run nav", -> - cy - .get(".navbar-default a") - .contains("Runs").should("have.class", "active") + cy.get(".navbar-default a") + .contains("Runs").should("have.class", "active") context "api server connection", -> beforeEach -> + timestamp = moment("2016-12-19T10:00:00").valueOf() + cy.clock(timestamp) @getCurrentUser.resolve(@user) @openProject.resolve(@config) @getRuns.resolve(@runs) @@ -78,6 +80,50 @@ describe "Runs List", -> it "shows runs", -> cy.contains("h5", "Runs") + context "displays each run's data", -> + beforeEach -> + cy.get(".runs-container li").first().as("firstRunRow") + cy.get(".runs-container li").eq(1).as("runRow") + + it "displays build num", -> + cy.get("@runRow").contains("#" + @runs[1].buildNumber) + + it "displays commit info", -> + cy.get("@runRow").contains(@runs[1].commit.branch) + cy.get("@runRow").contains(@runs[1].commit.message) + + it "displays platform info", -> + cy.get("@runRow").within -> + cy.contains(@runs[1].instances[0].platform.osVersionFormatted) + cy.contains(@runs[1].instances[0].platform.browserVersion) + cy.get(".fa-windows") + cy.get(".fa-chrome") + + it "displays totals", -> + cy.get("@runRow").contains(@runs[1].totalFailed) + cy.get("@runRow").contains(@runs[1].totalPassed) + + it "displays times", -> + cy.get("@runRow").contains("a few secs ago") + cy.get("@runRow").contains("00:12") + + it "displays seperate timers for incomplete runs", -> + cy.get("@firstRunRow").contains("47:02") + cy.get(".runs-container li").last().contains("57:02") + .then -> cy.tick(1000) + cy.get("@firstRunRow").contains("47:03") + cy.get(".runs-container li").last().contains("57:03") + + context "spec display", -> + it "displays spec if defined when 1 instance", -> + cy.get(".runs-container li").eq(1).contains(@runs[1].instances[0].spec) + + it "does not display spec if null", -> + cy.get(".runs-container li").eq(3).contains("spec").should("not.exist") + + it "does not display spec if multiple instances", -> + cy.get(".runs-container li").eq(2).contains("spec").should("not.exist") + describe "failure", -> beforeEach -> @pingApiServerAgain = @util.deferred() @@ -129,8 +175,7 @@ describe "Runs List", -> timestamp = new Date(2016, 11, 19, 10, 0, 0).valueOf() - cy - .clock(timestamp) + cy.clock(timestamp) .then => @goToRuns() .then => @@ -140,30 +185,26 @@ describe "Runs List", -> expect(@ipc.getRuns).to.be.called it "lists runs", -> - cy - .get(".runs-container li") + cy.get(".runs-container li") .should("have.length", @runs.length) it "displays link to dashboard that goes to admin project runs", -> - cy - .contains("See All").click() + cy.contains("See all").click() .then -> expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/dashboard/projects/#{@projects[0].id}/runs") it "displays run status icon", -> - cy - .get(".runs-container li").first().find("> div") + cy.get(".runs-container li").first().find("> div") .should("have.class", "running") it "displays last updated", -> cy.contains("Last updated: 10:00:00am") it "clicking run opens admin", -> - cy - .get(".runs-container li").first() + cy.get(".runs-container li").first() .click() .then => - expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/dashboard/projects/#{@projects[0].id}/runs/#{@runs[0].id}") + expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/dashboard/projects/#{@projects[0].id}/runs/#{@runs[0].buildNumber}") context "without a current user", -> beforeEach -> @@ -185,7 +226,7 @@ describe "Runs List", -> @goToRuns() it "displays 'need to set up' message", -> - cy.contains("You Have No Recorded Runs") + cy.contains("You have no recorded runs") context "without a current user and without project id", -> beforeEach -> @@ -196,14 +237,14 @@ describe "Runs List", -> @goToRuns() it "displays 'need to set up' message", -> - cy.contains("You Have No Recorded Runs") + cy.contains("You have no recorded runs") describe "click setup project", -> beforeEach -> - cy.contains("Set Up Project").click() + cy.contains("Set up project").click() it "shows login message", -> - cy.get(".login h1").should("contain", "Log In") + cy.get(".login h1").should("contain", "Log in") it "clicking 'Log In with GitHub' opens login", -> cy.contains("button", "Log In with GitHub").click().then -> @@ -218,21 +259,19 @@ describe "Runs List", -> @getRunsAgain = @util.deferred() @ipc.getRuns.onCall(1).returns(@getRunsAgain.promise) - cy - .clock() + cy.clock() .then => @goToRuns() .then => @getRuns.resolve(@runs) - .get(".runs-container") ## wait for original runs to show - .clock().then (clock) => + cy.get(".runs-container") ## wait for original runs to show + cy.clock().then (clock) => @getRunsAgain = @util.deferred() @ipc.getRuns.onCall(1).returns(@getRunsAgain.promise) - .tick(10000) + cy.tick(10000) it "has original state of runs", -> - cy - .get(".runs-container li").first().find("> div") + cy.get(".runs-container li").first().find("> div") .should("have.class", "running") it "sends get:runs ipc event", -> @@ -250,8 +289,7 @@ describe "Runs List", -> @getRunsAgain.resolve(@runs) it "updates the runs", -> - cy - .get(".runs-container li").first().find("> div") + cy.get(".runs-container li").first().find("> div") .should("have.class", "passed") it "enables refresh button", -> @@ -272,11 +310,11 @@ describe "Runs List", -> it "displays 'need to set up' message", -> @ipcError({type: "NO_PROJECT_ID"}) - cy.contains("You Have No Recorded Runs") + cy.contains("You have no recorded runs") it "displays old runs if another error", -> @ipcError({type: "TIMED_OUT"}) - cy.get(".runs-container li").should("have.length", 4) + cy.get(".runs-container li").should("have.length", @runs.length) describe "manually refreshing runs", -> beforeEach -> @@ -294,7 +332,7 @@ describe "Runs List", -> expect(@ipc.getRuns).to.be.calledTwice it "still shows list of runs", -> - cy.get(".runs-container li").should("have.length", 4) + cy.get(".runs-container li").should("have.length", @runs.length) it "disables refresh button", -> cy.get(".runs header button").should("be.disabled") @@ -327,7 +365,7 @@ describe "Runs List", -> @getRuns.reject({name: "foo", message: "There's an error", statusCode: 403}) it "displays permissions message", -> - cy.contains("Request Access") + cy.contains("Request access") context "any case", -> beforeEach -> @@ -337,33 +375,33 @@ describe "Runs List", -> context "request access", -> beforeEach -> - cy.contains("Request Access").click() + cy.contains("button", "Request access").as("requestAccessBtn").click() - it "sends requests access with org id", -> - expect(@ipc.requestAccess).to.be.calledWith("d8104707-a348-4653-baea-7da9c7d52448") + it "sends requests access with project id", -> + expect(@ipc.requestAccess).to.be.calledWith(@config.projectId) it "disables button", -> - cy.contains("Request Access").should("be.disabled") + cy.get("@requestAccessBtn").should("be.disabled") - it "hides 'Request Access' text", -> - cy.contains("Request Access").find("span").should("not.be.visible") + it "hides 'Request access' text", -> + cy.get("@requestAccessBtn").find("span").should("not.be.visible") it "shows spinner", -> - cy.contains("Request Access").find("> i").should("be.visible") + cy.get("@requestAccessBtn").find("> i").should("be.visible") describe "when request succeeds", -> beforeEach -> @requestAccess.resolve() it "shows success message", -> - cy.contains("Request Sent") + cy.contains("Request sent") it "'persists' request state (until app is reloaded at least)", -> @ipc.getRuns.onCall(1).rejects({name: "foo", message: "There's an error", statusCode: 403}) cy.get(".navbar-default a").contains("Tests").click() cy.get(".navbar-default a").contains("Runs").click() - cy.contains("Request Sent") + cy.contains("Request sent") describe "when request succeeds and user is already a member", -> beforeEach -> @@ -380,8 +418,7 @@ describe "Runs List", -> it "shows runs when getting runs succeeds", -> @getRuns.resolve(@runs) - cy - .get(".runs-container li") + cy.get(".runs-container li") .should("have.length", @runs.length) describe "when request fails", -> @@ -397,29 +434,30 @@ describe "Runs List", -> ## this is displayed in the DOM cy.contains("Request Failed") cy.contains("off the cracker") + cy.contains("button", "Request access").as("requestAccessBtn") it "enables button", -> - cy.contains("Request Access").should("not.be.disabled") + cy.get("@requestAccessBtn").should("not.be.disabled") - it "shows 'Request Access' text", -> - cy.contains("Request Access").find("span").should("be.visible") + it "shows 'Request access' text", -> + cy.get("@requestAccessBtn").find("span").should("be.visible") it "hides spinner", -> - cy.contains("Request Access").find("> i").should("not.be.visible") + cy.get("@requestAccessBtn").find("> i").should("not.be.visible") describe "because requested was denied", -> beforeEach -> @requestAccess.reject({type: "DENIED", name: "foo", message: "There's an error"}) it "shows 'success' message", -> - cy.contains("Request Sent") + cy.contains("Request sent") describe "because request was already sent", -> beforeEach -> @requestAccess.reject({type: "ALREADY_REQUESTED", name: "foo", message: "There's an error"}) it "shows 'success' message", -> - cy.contains("Request Sent") + cy.contains("Request sent") describe "because user became unauthenticated", -> beforeEach -> @@ -429,7 +467,7 @@ describe "Runs List", -> cy.shouldBeLoggedOut() it "shows login message", -> - cy.get(".empty h4").should("contain", "Log In") + cy.get(".empty h4").should("contain", "Log in") it "clicking 'Log In with GitHub' opens login", -> cy.contains("button", "Log In with GitHub").click().then -> @@ -452,7 +490,7 @@ describe "Runs List", -> @getRuns.reject({name: "foo", message: "There's an error", type: "NOT_FOUND"}) it "displays empty message", -> - cy.contains("Runs Cannot Be Displayed") + cy.contains("Runs cannot be displayed") describe "unauthenticated error", -> beforeEach -> @@ -464,7 +502,7 @@ describe "Runs List", -> cy.shouldBeLoggedOut() it "shows login message", -> - cy.get(".empty h4").should("contain", "Log In") + cy.get(".empty h4").should("contain", "Log in") describe "no project id error", -> beforeEach -> @@ -475,16 +513,15 @@ describe "Runs List", -> @getRuns.reject({name: "foo", message: "There's an error", type: "NO_PROJECT_ID"}) it "displays 'need to set up' message", -> - cy.contains("You Have No Recorded Runs") + cy.contains("You have no recorded runs") it "clears message after setting up to record", -> - cy - .contains(".btn", "Set Up Project").click() - .get(".modal-body") - .contains(".btn", "Me").click() - .get(".privacy-radio").find("input").last().check() - .get(".modal-body") - .contains(".btn", "Set Up Project").click() + cy.contains(".btn", "Set up project").click() + cy.get(".modal-body") + .contains(".btn", "Me").click() + cy.get(".privacy-radio").find("input").last().check() + cy.get(".modal-body") + .contains(".btn", "Set up project").click() cy.contains("To record your first") describe "unexpected error", -> @@ -525,21 +562,19 @@ describe "Runs List", -> @goToRuns() it "displays empty message", -> - cy.contains("Runs Cannot Be Displayed") + cy.contains("Runs cannot be displayed") it "clicking link opens setup project window", -> - cy - .contains(".btn", "Set Up a New Project").click() - .get(".modal").should("be.visible") + cy.contains(".btn", "Set up a new project").click() + cy.get(".modal").should("be.visible") it "clears message after setting up CI", -> - cy - .contains(".btn", "Set Up a New Project").click() - .get(".modal-body") - .contains(".btn", "Me").click() - .get(".privacy-radio").find("input").last().check() - .get(".modal-body") - .contains(".btn", "Set Up Project").click() + cy.contains(".btn", "Set up a new project").click() + cy.get(".modal-body") + .contains(".btn", "Me").click() + cy.get(".privacy-radio").find("input").last().check() + cy.get(".modal-body") + .contains(".btn", "Set up project").click() cy.contains("To record your first") describe "no runs", -> @@ -552,7 +587,7 @@ describe "Runs List", -> @getRuns.resolve([]) it "displays 'need to set up' message", -> - cy.contains("You Have No Recorded Runs") + cy.contains("You have no recorded runs") context "having previously set up CI", -> beforeEach -> @@ -564,13 +599,11 @@ describe "Runs List", -> cy.contains("To record your first") it "opens project id guide on clicking 'Why?'", -> - cy - .contains("Why?").click() + cy.contains("Why?").click() .then -> expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/what-is-a-project-id") it "opens dashboard on clicking 'Cypress Dashboard'", -> - cy - .contains("Cypress Dashboard").click() + cy.contains("Cypress Dashboard").click() .then -> expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/dashboard/projects/#{@config.projectId}/runs") diff --git a/packages/desktop-gui/cypress/integration/settings_spec.coffee b/packages/desktop-gui/cypress/integration/settings_spec.coffee index 984b1451fad6..b6ceb975a130 100644 --- a/packages/desktop-gui/cypress/integration/settings_spec.coffee +++ b/packages/desktop-gui/cypress/integration/settings_spec.coffee @@ -16,7 +16,7 @@ describe "Settings", -> cy.visitIndex().then (win) -> { start, @ipc } = win.App - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "getCurrentUser").resolves(@user) cy.stub(@ipc, "updaterCheck").resolves(false) cy.stub(@ipc, "getSpecs").yields(null, @specs) diff --git a/packages/desktop-gui/cypress/integration/setup_project_modal_spec.coffee b/packages/desktop-gui/cypress/integration/setup_project_modal_spec.coffee index cb0665808620..3cbe2a4e583b 100644 --- a/packages/desktop-gui/cypress/integration/setup_project_modal_spec.coffee +++ b/packages/desktop-gui/cypress/integration/setup_project_modal_spec.coffee @@ -11,7 +11,7 @@ describe "Set Up Project", -> cy.visitIndex().then (win) -> { start, @ipc } = win.App - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "updaterCheck").resolves(false) cy.stub(@ipc, "closeBrowser").resolves(null) @config.projectId = null @@ -40,7 +40,7 @@ describe "Set Up Project", -> .contains("Runs").click() it "displays 'need to set up' message", -> - cy.contains("You Have No Recorded Runs") + cy.contains("You have no recorded runs") describe "when there is a current user", -> beforeEach -> @@ -49,13 +49,13 @@ describe "Set Up Project", -> describe "general behavior", -> beforeEach -> @getOrgs.resolve(@orgs) - cy.get(".btn").contains("Set Up Project").click() + cy.get(".btn").contains("Set up project").click() it "clicking link opens setup project window", -> cy.get(".modal").should("be.visible") it "submit button is disabled", -> - cy.get(".modal").contains(".btn", "Set Up Project") + cy.get(".modal").contains(".btn", "Set up project") .should("be.disabled") it "prefills Project Name", -> @@ -94,7 +94,7 @@ describe "Set Up Project", -> context "with orgs", -> beforeEach -> @getOrgs.resolve(@orgs) - cy.get(".btn").contains("Set Up Project").click() + cy.get(".btn").contains("Set up project").click() cy.get(".modal-content") .contains(".btn", "An Organization").click() @@ -127,7 +127,7 @@ describe "Set Up Project", -> context "without orgs", -> beforeEach -> @getOrgs.resolve([]) - cy.get(".btn").contains("Set Up Project").click() + cy.get(".btn").contains("Set up project").click() cy.get(".modal-content") .contains(".btn", "An Organization").click() @@ -135,7 +135,7 @@ describe "Set Up Project", -> cy.get(".empty-select-orgs").should("be.visible") it "opens dashboard organizations when 'create org' is clicked", -> - cy.contains("Create Organization").click().then -> + cy.contains("Create organization").click().then -> expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/dashboard/organizations") context "without only default org", -> @@ -145,7 +145,7 @@ describe "Set Up Project", -> "name": "Jane Lane", "default": true }]) - cy.get(".btn").contains("Set Up Project").click() + cy.get(".btn").contains("Set up project").click() cy.get(".modal-content") .contains(".btn", "An Organization").click() @@ -153,14 +153,14 @@ describe "Set Up Project", -> cy.get(".empty-select-orgs").should("be.visible") it "opens dashboard organizations when 'create org' is clicked", -> - cy.contains("Create Organization").click().then -> + cy.contains("Create organization").click().then -> expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/dashboard/organizations") context "polls for updates to organizations", -> beforeEach -> cy.clock() @getOrgs.resolve(@orgs) - cy.get(".btn").contains("Set Up Project").click() + cy.get(".btn").contains("Set up project").click() cy.get(".modal-content") .contains(".btn", "An Organization").click() @@ -192,21 +192,21 @@ describe "Set Up Project", -> describe "on submit", -> beforeEach -> @getOrgs.resolve(@orgs) - cy.contains(".btn", "Set Up Project").click() + cy.contains(".btn", "Set up project").click() cy.get(".modal-body") .contains(".btn", "Me").click() cy.get(".privacy-radio").find("input").last().check() cy.get(".modal-body") - .contains(".btn", "Set Up Project").click() + .contains(".btn", "Set up project").click() it "disables button", -> cy.get(".modal-body") - .contains(".btn", "Set Up Project") + .contains(".btn", "Set up project") .should("be.disabled") it "shows spinner", -> cy.get(".modal-body") - .contains(".btn", "Set Up Project") + .contains(".btn", "Set up project") .find("i") .should("be.visible") @@ -219,7 +219,7 @@ describe "Set Up Project", -> orgId: "000" }) - cy.contains(".btn", "Set Up Project").click() + cy.contains(".btn", "Set up project").click() it "sends project name, org id, and public flag to ipc event", -> cy.get(".modal-body") @@ -228,7 +228,7 @@ describe "Set Up Project", -> cy.get("select").select("Acme Developers") cy.get(".privacy-radio").find("input").first().check() cy.get(".modal-body") - .contains(".btn", "Set Up Project").click() + .contains(".btn", "Set up project").click() .then => expect(@ipc.setupDashboardProject).to.be.calledWith({ projectName: "New Project" @@ -243,7 +243,7 @@ describe "Set Up Project", -> cy.get("select").select("Acme Developers") cy.get(".privacy-radio").find("input").first().check() cy.get(".modal-body") - .contains(".btn", "Set Up Project").click() + .contains(".btn", "Set up project").click() it "sends data from form to ipc event", -> expect(@ipc.setupDashboardProject).to.be.calledWith({ @@ -258,7 +258,7 @@ describe "Set Up Project", -> .contains(".btn", "Me").click() cy.get(".privacy-radio").find("input").last().check() cy.get(".modal-body") - .contains(".btn", "Set Up Project").click() + .contains(".btn", "Set up project").click() it "sends data from form to ipc event", -> expect(@ipc.setupDashboardProject).to.be.calledWith({ @@ -273,7 +273,7 @@ describe "Set Up Project", -> .contains(".btn", "Me").click() cy.get(".privacy-radio").find("input").first().check() cy.get(".modal-body") - .contains(".btn", "Set Up Project").click() + .contains(".btn", "Set up project").click() it "sends data from form to ipc event", -> expect(@ipc.setupDashboardProject).to.be.calledWith({ @@ -297,12 +297,12 @@ describe "Set Up Project", -> describe "errors", -> beforeEach -> @getOrgs.resolve(@orgs) - cy.contains(".btn", "Set Up Project").click() + cy.contains(".btn", "Set up project").click() cy.get(".modal-body") .contains(".btn", "Me").click() cy.get(".privacy-radio").find("input").last().check() cy.get(".modal-body") - .contains(".btn", "Set Up Project").click() + .contains(".btn", "Set up project").click() it "logs user out when 401", -> @setupDashboardProject.reject({ name: "", message: "", statusCode: 401 }) @@ -322,7 +322,7 @@ describe "Set Up Project", -> describe "when get orgs 401s", -> beforeEach -> - cy.contains(".btn", "Set Up Project").click() + cy.contains(".btn", "Set up project").click() .then => @getOrgs.reject({ name: "", message: "", statusCode: 401 }) @@ -332,7 +332,7 @@ describe "Set Up Project", -> describe "when there is no current user", -> beforeEach -> @getCurrentUser.resolve(null) - cy.get(".btn").contains("Set Up Project").click() + cy.get(".btn").contains("Set up project").click() it "shows login", -> cy.get(".modal").contains("Log In with GitHub") @@ -344,4 +344,4 @@ describe "Set Up Project", -> cy.contains("button", "Log In with GitHub").click() it "shows setup", -> - cy.contains("h4", "Set Up Project") + cy.contains("h4", "Set up project") diff --git a/packages/desktop-gui/cypress/integration/specs_list_spec.coffee b/packages/desktop-gui/cypress/integration/specs_list_spec.coffee index 039180e4bbe7..1abf067b6f25 100644 --- a/packages/desktop-gui/cypress/integration/specs_list_spec.coffee +++ b/packages/desktop-gui/cypress/integration/specs_list_spec.coffee @@ -8,7 +8,7 @@ describe "Specs List", -> cy.visitIndex().then (win) -> { start, @ipc } = win.App - cy.stub(@ipc, "getOptions").resolves({projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({projectRoot: "/foo/bar"}) cy.stub(@ipc, "getCurrentUser").resolves(@user) cy.stub(@ipc, "getSpecs").yields(null, @specs) cy.stub(@ipc, "closeBrowser").resolves(null) @@ -35,8 +35,7 @@ describe "Specs List", -> cy.contains(@config.integrationFolder) it "triggers open:finder on click of text folder", -> - cy - .contains(@config.integrationFolder).click().then -> + cy.contains(@config.integrationFolder).click().then -> expect(@ipc.openFinder).to.be.calledWith(@config.integrationFolder) it "displays help link", -> @@ -56,37 +55,41 @@ describe "Specs List", -> it "displays the scaffolded files", -> cy.get(".folder-preview-onboarding").within -> - cy.contains("fixtures") + cy.contains("span", "fixtures").siblings("ul").within -> cy.contains("example.json") - cy.contains("integration") - cy.contains("example_spec.js") - cy.contains("support") - cy.contains("commands.js") - cy.contains("defaults.js") - cy.contains("index.js") + cy.contains("span", "integration").siblings("ul").within -> + cy.contains("examples") + cy.contains("span", "support").siblings("ul").within -> + cy.contains("commands.js") + cy.contains("defaults.js") + cy.contains("index.js") + cy.contains("span", "plugins").siblings("ul").within -> + cy.contains("index.js") it "lists folders and files alphabetically", -> cy.get(".folder-preview-onboarding").within -> - cy - .contains("fixtures").parent().next() + cy.contains("fixtures").parent().next() .contains("integration") + it "truncates file lists with more than 3 items", -> + cy.get(".folder-preview-onboarding").within -> + cy.contains("examples").closest(".new-item").find("li") + .should("have.length", 3) + cy.get(".is-more").should("have.text", " ... 17 more files ...") + it "can dismiss the modal", -> - cy - .contains("OK, got it!").click() - .get(".modal").should("not.be.visible") + cy.contains("OK, got it!").click() + cy.get(".modal").should("not.be.visible") .then -> expect(@ipc.onboardingClosed).to.be.called - it "triggers open:finder on click of example file", -> - cy - .get(".modal").contains("example_spec.js").click().then -> - expect(@ipc.openFinder).to.be.calledWith(@config.integrationExampleFile) + it "triggers open:finder on click of example folder", -> + cy.get(".modal").contains("examples").click().then => + expect(@ipc.openFinder).to.be.calledWith(@config.integrationExamplePath) it "triggers open:finder on click of text folder", -> - cy - .get(".modal").contains("cypress/integration").click().then -> - expect(@ipc.openFinder).to.be.calledWith(@config.integrationFolder) + cy.get(".modal").contains("cypress/integration").click().then => + expect(@ipc.openFinder).to.be.calledWith(@config.integrationFolder) describe "lists specs", -> context "Windows paths", -> @@ -109,16 +112,16 @@ describe "Specs List", -> context "run all specs", -> it "displays run all specs button", -> - cy.contains(".btn", "Run All Tests") + cy.contains(".btn", "Run all tests") it "has play icon", -> cy - .contains(".btn", "Run All Tests") + .contains(".btn", "Run all tests") .find("i").should("have.class", "fa-play") it "triggers browser launch on click of button", -> cy - .contains(".btn", "Run All Tests").click() + .contains(".btn", "Run all tests").click() .then -> launchArgs = @ipc.launchBrowser.lastCall.args @@ -127,7 +130,7 @@ describe "Specs List", -> describe "all specs running in browser", -> beforeEach -> - cy.contains(".btn", "Run All Tests").as("allSpecs").click() + cy.contains(".btn", "Run all tests").as("allSpecs").click() it "updates spec icon", -> cy.get("@allSpecs").find("i").should("have.class", "fa-dot-circle-o") @@ -199,26 +202,60 @@ describe "Specs List", -> cy.get(lastExpandedFolderSelector).click() cy.get(".file").should("have.length", 0) - context "Searching specs", -> - beforeEach -> - @ipc.getSpecs.yields(null, @specs) - @openProject.resolve(@config) - cy.get("#search-input").type("new") - - it "should display only one spec", -> - cy.get(".list-as-table .file") - .should("have.length", 1) - .and("contain", "account_new_spec.coffee") - - it "should display the same number of open folders", -> - cy.get(".list-as-table .folder") - .should("have.length", 10) - - it "should clear the search if the user press ESC key", -> - cy.get("#search-input").type("{esc}") - .should("have.value", "") - cy.get(".list-as-table .file") - .should("have.length", 7) + context "filtering specs", -> + describe "typing the filter", -> + beforeEach -> + @ipc.getSpecs.yields(null, @specs) + @openProject.resolve(@config) + cy.get(".filter").type("new") + + it "displays only matching spec", -> + cy.get(".outer-files-container .file") + .should("have.length", 1) + .and("contain", "account_new_spec.coffee") + + it "only shows matching folders", -> + cy.get(".outer-files-container .folder") + .should("have.length", 2) + + it "clears the filter on clear button click", -> + cy.get(".clear-filter").click() + cy.get(".filter") + .should("have.value", "") + cy.get(".outer-files-container .file") + .should("have.length", 7) + + it "clears the filter if the user press ESC key", -> + cy.get(".filter").type("{esc}") + .should("have.value", "") + cy.get(".outer-files-container .file") + .should("have.length", 7) + + it "shows empty message if no results", -> + cy.get(".filter").clear().type("foobarbaz") + cy.get(".outer-files-container").should("not.exist") + cy.get(".empty-well").should("have.text", "No files match the filter 'foobarbaz'") + + it "saves the filter to local storage for the project", -> + cy.window().then (win) => + expect(win.localStorage["specsFilter-#{@config.projectId}"]).to.be.a("string") + expect(JSON.parse(win.localStorage["specsFilter-#{@config.projectId}"])).to.equal("new") + + describe "when there's a saved filter", -> + beforeEach -> + @ipc.getSpecs.yields(null, @specs) + cy.window().then (win) -> + win.localStorage["specsFilter-#{@config.projectId}"] = JSON.stringify("app") + + it "applies it for the appropriate project", -> + @openProject.resolve(@config) + cy.get(".filter").should("have.value", "app") + + it "does not apply it for a different project", -> + @config.projectId = "different" + @openProject.resolve(@config) + cy.get(".filter").should("have.value", "") + context "click on spec", -> beforeEach -> diff --git a/packages/desktop-gui/cypress/integration/update_banner_spec.coffee b/packages/desktop-gui/cypress/integration/update_banner_spec.coffee index bfc74e6e7d60..7d660e29736b 100644 --- a/packages/desktop-gui/cypress/integration/update_banner_spec.coffee +++ b/packages/desktop-gui/cypress/integration/update_banner_spec.coffee @@ -79,7 +79,7 @@ describe "Update Banner", -> describe "in project mode", -> beforeEach -> - cy.stub(@ipc, "getOptions").resolves({version: OLD_VERSION, projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({version: OLD_VERSION, projectRoot: "/foo/bar"}) @start() @updaterCheck.resolve(NEW_VERSION) cy.contains("Update").click() @@ -97,7 +97,7 @@ describe "Update Banner", -> describe "in specs list", -> beforeEach -> - cy.stub(@ipc, "getOptions").resolves({version: OLD_VERSION, projectPath: "/foo/bar"}) + cy.stub(@ipc, "getOptions").resolves({version: OLD_VERSION, projectRoot: "/foo/bar"}) cy.stub(@ipc, "openProject").resolves(@config) cy.stub(@ipc, "getSpecs").yields(null, @specs) @start() diff --git a/packages/desktop-gui/cypress/integration/utils_spec.coffee b/packages/desktop-gui/cypress/integration/utils_spec.coffee new file mode 100644 index 000000000000..95baf26f01e3 --- /dev/null +++ b/packages/desktop-gui/cypress/integration/utils_spec.coffee @@ -0,0 +1,24 @@ +{ durationFormatted, stripLeadingCyDirs } = require("../../src/lib/utils") + +describe "durationFormatted", -> + it "formats ms", -> + expect(durationFormatted(496)).to.eq('496ms') + + it "formats 1 digit secs", -> + expect(durationFormatted(1000)).to.eq('00:01') + + it "formats 2 digit secs", -> + expect(durationFormatted(21000)).to.eq('00:21') + + it "formats mins and secs", -> + expect(durationFormatted(321000)).to.eq('05:21') + + it "formats 2 digit mins and secs", -> + expect(durationFormatted(3330000)).to.eq('55:30') + + it "formats hours with mins", -> + expect(durationFormatted(33300000)).to.eq('9:15:00') + +describe "stripLeadingCyDirs", -> + it "strips leading cypress directories from spec", -> + expect(stripLeadingCyDirs('cypress/integration/login_spec.js')).to.eq('login_spec.js') \ No newline at end of file diff --git a/packages/desktop-gui/jsconfig.json b/packages/desktop-gui/jsconfig.json new file mode 100644 index 000000000000..504cd646e149 --- /dev/null +++ b/packages/desktop-gui/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + } +} diff --git a/packages/desktop-gui/package.json b/packages/desktop-gui/package.json index 67bf2ba2f568..9f36142b139c 100644 --- a/packages/desktop-gui/package.json +++ b/packages/desktop-gui/package.json @@ -20,6 +20,7 @@ ], "devDependencies": { "@cypress/icons": "0.5.4", + "@cypress/json-schemas": "5.0.1", "@cypress/react-tooltip": "^0.2.2", "@cypress/releaser": "0.1.12", "bin-up": "^1.0.0", @@ -43,6 +44,7 @@ "react-bootstrap-modal": "3.0.1", "react-dom": "^15.6.1", "react-loader": "^2.4.0", + "string.prototype.padstart": "^3.0.0", "zunder": "5.6.5" } } diff --git a/packages/desktop-gui/src/app/app.jsx b/packages/desktop-gui/src/app/app.jsx index 4728f0ff1748..1e46d6b8ee37 100644 --- a/packages/desktop-gui/src/app/app.jsx +++ b/packages/desktop-gui/src/app/app.jsx @@ -20,7 +20,7 @@ class App extends Component { appApi.listenForMenuClicks() ipc.getOptions().then((options = {}) => { - appStore.set(_.pick(options, 'cypressEnv', 'os', 'projectPath', 'version')) + appStore.set(_.pick(options, 'cypressEnv', 'os', 'projectRoot', 'version')) viewStore.showApp() }) diff --git a/packages/desktop-gui/src/duration-timer/TimerDisplay.jsx b/packages/desktop-gui/src/duration-timer/TimerDisplay.jsx new file mode 100644 index 000000000000..c8d282dabefd --- /dev/null +++ b/packages/desktop-gui/src/duration-timer/TimerDisplay.jsx @@ -0,0 +1,29 @@ +import React, { Component } from 'react' +import { observer } from 'mobx-react' +import TimerStore from './duration-timer-store' + +@observer +export default class TimerDisplay extends Component { + constructor (...props) { + super(...props) + + this.timerStore = new TimerStore(this.props.startTime) + } + + componentDidMount () { + this.timerStore.startTimer() + } + + componentWillUnmount () { + this.timerStore.resetTimer() + } + + render () { + return ( + <span> + <i className='fa fa-hourglass-end'></i>{' '} + {this.timerStore.mainDisplay} + </span> + ) + } +} diff --git a/packages/desktop-gui/src/duration-timer/duration-timer-model.js b/packages/desktop-gui/src/duration-timer/duration-timer-model.js new file mode 100644 index 000000000000..1ca4bab19523 --- /dev/null +++ b/packages/desktop-gui/src/duration-timer/duration-timer-model.js @@ -0,0 +1,20 @@ +import { observable, computed, action } from 'mobx' +import { uniqueId } from 'lodash' +import { durationFormatted } from '../lib/utils' + +export default class Timer { + @observable milliseconds + + constructor (initialMilliseconds = 0) { + this.milliseconds = initialMilliseconds + this.id = uniqueId() + } + + @action reset () { + this.milliseconds = 0 + } + + @computed get display () { + return durationFormatted(this.milliseconds) + } +} diff --git a/packages/desktop-gui/src/duration-timer/duration-timer-store.js b/packages/desktop-gui/src/duration-timer/duration-timer-store.js new file mode 100644 index 000000000000..82e8ab8d80d5 --- /dev/null +++ b/packages/desktop-gui/src/duration-timer/duration-timer-store.js @@ -0,0 +1,41 @@ +import { observable, computed, action } from 'mobx' +import moment from 'moment' + +import Timer from './duration-timer-model' + +class DurationTimer { + @observable isRunning = false + @observable timer + @observable startTime + + constructor (startTime) { + this.timer = new Timer() + this.startTime = moment(startTime) + } + + @computed get mainDisplay () { + return this.timer.display + } + + @action measure () { + if (!this.isRunning) return + + this.timer.milliseconds = moment().diff(this.startTime) + + this.timerId = setTimeout(() => this.measure(), 10) + } + + @action startTimer () { + if (this.isRunning) return + this.isRunning = true + this.measure() + } + + @action resetTimer () { + this.timer.reset() + this.isRunning = false + clearTimeout(this.timerId) + } +} + +export default DurationTimer diff --git a/packages/desktop-gui/src/lib/app-store.js b/packages/desktop-gui/src/lib/app-store.js index e243d9cf5662..529c4178ea7f 100644 --- a/packages/desktop-gui/src/lib/app-store.js +++ b/packages/desktop-gui/src/lib/app-store.js @@ -4,7 +4,7 @@ import localData from '../lib/local-data' class AppStore { @observable cypressEnv @observable os - @observable projectPath = null + @observable projectRoot = null @observable newVersion @observable version @observable localInstallNoticeDismissed = localData.get('local-install-notice-dimissed') @@ -19,7 +19,7 @@ class AppStore { } @computed get isGlobalMode () { - return !this.projectPath + return !this.projectRoot } @computed get updateAvailable () { @@ -29,7 +29,7 @@ class AppStore { @action set (props) { if (props.cypressEnv != null) this.cypressEnv = props.cypressEnv if (props.os != null) this.os = props.os - if (props.projectPath != null) this.projectPath = props.projectPath + if (props.projectRoot != null) this.projectRoot = props.projectRoot if (props.version != null) this.version = this.newVersion = props.version } diff --git a/packages/desktop-gui/src/lib/configure-moment.js b/packages/desktop-gui/src/lib/configure-moment.js index 3005cedb9cab..c60653365e2b 100644 --- a/packages/desktop-gui/src/lib/configure-moment.js +++ b/packages/desktop-gui/src/lib/configure-moment.js @@ -5,7 +5,8 @@ const configureMoment = () => { relativeTime: { future: 'in %s', past: '%s ago', - s: 'secs', + s: 'a few secs', + ss: '%d secs', m: 'a min', mm: '%d mins', h: 'an hour', diff --git a/packages/desktop-gui/src/lib/local-data.js b/packages/desktop-gui/src/lib/local-data.js index d4aa5dacce7d..4027b6f12dde 100644 --- a/packages/desktop-gui/src/lib/local-data.js +++ b/packages/desktop-gui/src/lib/local-data.js @@ -7,4 +7,8 @@ export default { set (key, value) { localStorage[key] = JSON.stringify(value) }, + + remove (key) { + localStorage.removeItem(key) + }, } diff --git a/packages/desktop-gui/src/lib/utils.js b/packages/desktop-gui/src/lib/utils.js index ff895dfba208..f3eaebe179b2 100644 --- a/packages/desktop-gui/src/lib/utils.js +++ b/packages/desktop-gui/src/lib/utils.js @@ -1,19 +1,16 @@ import _ from 'lodash' import moment from 'moment' import gravatar from 'gravatar' +import padStart from 'string.prototype.padstart' -const osNameLookup = { - darwin: 'apple', -} +const cyDirRegex = /^cypress\/integration\//g const osIconLookup = { - windows: 'windows', + win32: 'windows', darwin: 'apple', linux: 'linux', } -const browserNameLookup = {} - const browserIconLookup = { chrome: 'chrome', Electron: 'chrome', @@ -28,25 +25,12 @@ module.exports = { return osIconLookup[osName] || 'desktop' }, - osNameFormatted: (osName) => { - if (!osName) return '' - - return _.capitalize(osNameLookup[osName] || osName) - }, - browserIcon: (browserName) => { if (!browserName) return '' return browserIconLookup[browserName] || 'globe' }, - browserNameFormatted: (browserName) => { - if (!browserName) return '' - - return _.capitalize(browserNameLookup[browserName] || browserName) - }, - - browserVersionFormatted: (browserVersion) => { if (!browserVersion) return '' @@ -85,13 +69,31 @@ module.exports = { } }, - durationFormatted: (durationInMs) => { + durationFormatted: (durationInMs, padMinutes = true) => { const duration = moment.duration(durationInMs) - let durationHours = duration.hours() ? `${duration.hours()}h ` : '' - let durationMinutes = duration.minutes() ? `${duration.minutes()}m ` : '' - let durationSeconds = duration.seconds() ? `${duration.seconds()}s ` : '' + const durationSecs = duration.seconds() ? `${duration.seconds()}` : '' + const durationMins = duration.minutes() ? `${duration.minutes()}` : '' + const durationHrs = duration.hours() ? `${duration.hours()}` : '' + + const total = _.compact([ + durationHrs, + !!durationHrs || padMinutes ? padStart(durationMins, 2, '0') : durationMins, + padStart(durationSecs, 2, '0'), + ]) + + const totalMinSec = total.join(':') + + if (totalMinSec === '00:00') { + return `${duration.milliseconds()}ms` + } else { + return totalMinSec + } + }, - return durationHours + durationMinutes + durationSeconds + stripLeadingCyDirs (spec) { + if (!spec) return null + // remove leading 'cypress/integration' from spec + return spec.replace(cyDirRegex, '') }, } diff --git a/packages/desktop-gui/src/lib/view-store.js b/packages/desktop-gui/src/lib/view-store.js index e58c48bd1e9d..6aad29e7a413 100644 --- a/packages/desktop-gui/src/lib/view-store.js +++ b/packages/desktop-gui/src/lib/view-store.js @@ -16,8 +16,8 @@ class ViewStore { } @action showApp () { - if (appStore.projectPath) { - this.showProjectSpecs(projectsStore.getProjectByPath(appStore.projectPath)) + if (appStore.projectRoot) { + this.showProjectSpecs(projectsStore.getProjectByPath(appStore.projectRoot)) } else { this.showIntro() } diff --git a/packages/desktop-gui/src/project/onboarding.jsx b/packages/desktop-gui/src/project/onboarding.jsx index f74bb4657062..d1ecd60e4d25 100644 --- a/packages/desktop-gui/src/project/onboarding.jsx +++ b/packages/desktop-gui/src/project/onboarding.jsx @@ -1,3 +1,4 @@ +import cs from 'classnames' import _ from 'lodash' import React, { Component } from 'react' import { observer } from 'mobx-react' @@ -6,11 +7,18 @@ import BootstrapModal from 'react-bootstrap-modal' import ipc from '../lib/ipc' @observer -class OnBoading extends Component { - constructor (props) { - super(props) +class OnBoarding extends Component { + componentDidMount () { + this._maybeShowModal() + } + + componentDidUpdate () { + this._maybeShowModal() + } - if (this.props.project.isNew) { + _maybeShowModal () { + if (!this.showedModal && this.props.project.isNew) { + this.showedModal = true this.props.project.openModal() } } @@ -33,13 +41,13 @@ class OnBoading extends Component { <div className='empty-onboarding'> <h1>To help you get started...</h1> <p> - We've added some folders and example tests to your project. Try running the - <strong onClick={this._openExampleSpec.bind(this)}> - <i className='fa fa-file-code-o'></i>{' '} + 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>{' '} {project.integrationExampleName}{' '} </strong> - tests or add your own test file to - <strong onClick={this._openIntegrationFolder.bind(this)}> + folder or add your own test files to + <strong onClick={this._openIntegrationFolder}> <i className='fa fa-folder-o'></i>{' '} cypress/integration </strong>. @@ -75,10 +83,18 @@ class OnBoading extends Component { } _scaffoldedFiles (files, className) { - return _.map(_.sortBy(files, 'name'), (file) => { + files = _.sortBy(files, 'name') + + const notFolders = _.every(files, (file) => !file.children) + if (notFolders && files.length > 3) { + const numHidden = files.length - 2 + files = files.slice(0, 2).concat({ name: `... ${numHidden} more files ...`, more: true }) + } + + return _.map(files, (file) => { if (file.children) { return ( - <li className={className} key={file.name}> + <li className={cs(className, 'new-item')} key={file.name}> <span> <i className='fa fa-folder-open-o'></i>{' '} {file.name} @@ -90,7 +106,7 @@ class OnBoading extends Component { ) } else { return ( - <li className={className} key={file.name}> + <li className={cs(className, 'new-item', { 'is-more': file.more })} key={file.name}> <span> <i className='fa fa-file-code-o'></i>{' '} {file.name} @@ -101,13 +117,13 @@ class OnBoading extends Component { }) } - _openExampleSpec () { - ipc.openFinder(this.props.project.integrationExampleFile) + _openExampleSpec = () => { + ipc.openFinder(this.props.project.integrationExamplePath) } - _openIntegrationFolder () { + _openIntegrationFolder = () => { ipc.openFinder(this.props.project.integrationFolder) } } -export default OnBoading +export default OnBoarding diff --git a/packages/desktop-gui/src/project/onboarding.scss b/packages/desktop-gui/src/project/onboarding.scss index 4f23008f81e9..cc939162c1e4 100644 --- a/packages/desktop-gui/src/project/onboarding.scss +++ b/packages/desktop-gui/src/project/onboarding.scss @@ -86,7 +86,6 @@ border-top: 1px solid lighten($pass, 54%); background-color: lighten($pass, 58%); overflow: auto; - } &>ul { @@ -101,11 +100,10 @@ &>li { margin-bottom: 0; padding-left: 10px; - } } - ul:first-child>li>ul>li:last-child:after { + .new-item:after { color: lighten($pass, 5%); content: "+"; position: absolute; @@ -113,20 +111,27 @@ left: 10px; } - ul:first-child>li>ul>li>ul>li:after { - color: lighten($pass, 5%); - content: "+"; - position: absolute; + .new-item .new-item:after { top: -2px; left: -40px; } - ul:first-child>li>ul>li>ul>li>ul>li:after { - color: lighten($pass, 5%); - content: "+"; - position: absolute; + .new-item .new-item .new-item:after { top: -2px; left: -60px; } + + .new-item .new-item .new-item .new-item:after { + top: -2px; + left: -80px; + } + + .is-more { + i { + display: none; + } + + opacity: 0.7; + } } } diff --git a/packages/desktop-gui/src/project/project-model.js b/packages/desktop-gui/src/project/project-model.js index 2c5f0baf9b42..86623f76a1f5 100644 --- a/packages/desktop-gui/src/project/project-model.js +++ b/packages/desktop-gui/src/project/project-model.js @@ -174,11 +174,11 @@ export default class Project { @action setOnBoardingConfig (config) { this.isNew = config.isNewProject - this.integrationExampleFile = config.integrationExampleFile this.integrationFolder = config.integrationFolder this.parentTestsFolderDisplay = config.parentTestsFolderDisplay this.fileServerFolder = config.fileServerFolder this.integrationExampleName = config.integrationExampleName + this.integrationExamplePath = config.integrationExamplePath this.scaffoldedFiles = config.scaffoldedFiles } diff --git a/packages/desktop-gui/src/projects/projects-api.js b/packages/desktop-gui/src/projects/projects-api.js index eda7b8c4a9df..850cde05de6a 100644 --- a/packages/desktop-gui/src/projects/projects-api.js +++ b/packages/desktop-gui/src/projects/projects-api.js @@ -169,6 +169,7 @@ const openProject = (project) => { return ipc.openProject(project.path) .then((config = {}) => { updateConfig(config) + specsStore.setFilter(config.projectId, localData.get(`specsFilter-${config.projectId}`)) project.setLoading(false) getSpecs(setProjectError) diff --git a/packages/desktop-gui/src/runs/error-message.jsx b/packages/desktop-gui/src/runs/error-message.jsx index bf9aae96ec55..94fa480c6259 100644 --- a/packages/desktop-gui/src/runs/error-message.jsx +++ b/packages/desktop-gui/src/runs/error-message.jsx @@ -29,7 +29,7 @@ const ErrorMessage = observer(({ error }) => { <div className='empty'> <h4> <i className='fa fa-warning red'></i>{' '} - Runs Could Not Be Loaded + Runs could not be loaded </h4> {errorMessage} </div> diff --git a/packages/desktop-gui/src/runs/permission-message.jsx b/packages/desktop-gui/src/runs/permission-message.jsx index 3370ad615086..c79a7fd8c895 100644 --- a/packages/desktop-gui/src/runs/permission-message.jsx +++ b/packages/desktop-gui/src/runs/permission-message.jsx @@ -54,7 +54,7 @@ class PermissionMessage extends Component { > <span> <i className='fa fa-paper-plane'></i>{' '} - Request Access + Request access </span> <i className='fa fa-spinner fa-spin'></i> </button> @@ -66,7 +66,7 @@ class PermissionMessage extends Component { <div className='empty'> <h4> <i className='fa fa-check passed'></i>{' '} - Request Sent + Request sent </h4> <p> The project owner will be notified with your request. diff --git a/packages/desktop-gui/src/runs/project-not-setup.jsx b/packages/desktop-gui/src/runs/project-not-setup.jsx index ff1eb40df028..7cc0c108b726 100644 --- a/packages/desktop-gui/src/runs/project-not-setup.jsx +++ b/packages/desktop-gui/src/runs/project-not-setup.jsx @@ -40,7 +40,7 @@ export default class ProjectNotSetup extends Component { _getStartedWithCI () { return ( <div className='empty-no-runs'> - <h4>You Have No Recorded Runs</h4> + <h4>You have no recorded runs</h4> <p>Cypress can record screenshots, videos and failures when running <code>cypress run</code>.</p> <div className='runs-screenshots'> <img width='150' height='150' src='https://on.cypress.io/images/desktop-onboarding-thumb-1' /> @@ -53,7 +53,7 @@ export default class ProjectNotSetup extends Component { onClick={this._showSetupProjectModal} > <i className='fa fa-wrench'></i>{' '} - Set Up Project to Record + Set up project to record </button> </div> ) @@ -69,7 +69,7 @@ export default class ProjectNotSetup extends Component { <div className='empty-runs-not-displayed'> <h4> <i className='fa fa-warning errored'></i>{' '} - Runs Cannot Be Displayed + Runs cannot be displayed </h4> <p>We were unable to find an existing project matching the <code>projectId</code> in your <code>cypress.json</code>.</p> <p>To see runs for a current project, add the correct <code>projectId</code> to your <code>cypress.json</code></p> @@ -79,7 +79,7 @@ export default class ProjectNotSetup extends Component { onClick={this._showSetupProjectModal} > <i className='fa fa-wrench'></i>{' '} - Set Up a New Project + Set up a new project </button> <p> <small>The new project will have no previous run data.</small> diff --git a/packages/desktop-gui/src/runs/run-model.js b/packages/desktop-gui/src/runs/run-model.js index 6cb6a9aef503..8d83efb6a282 100644 --- a/packages/desktop-gui/src/runs/run-model.js +++ b/packages/desktop-gui/src/runs/run-model.js @@ -1,26 +1,10 @@ +import { assign } from 'lodash' import { observable } from 'mobx' export default class Run { @observable id - constructor (props) { - this.id = props.id - this.buildNumber = props.buildNumber - this.ciProvider = props.ciProvider - this.ciUrl = props.ciUrl - this.commitAuthorEmail = props.commitAuthorEmail - this.commitAuthorName = props.commitAuthorName - this.commitBranch = props.commitBranch - this.commitMessage = props.commitMessage - this.commitSha = props.commitSha - this.createdAt = props.createdAt - this.expectedInstances = props.expectedInstances - this.projectId = props.projectId - this.status = props.status - this.totalDuration = props.totalDuration - this.totalFailures = props.totalFailures - this.totalPasses = props.totalPasses - this.totalPending = props.totalPending - this.instances = props.instances + constructor (options) { + assign(this, options) } } diff --git a/packages/desktop-gui/src/runs/runs-list-item.jsx b/packages/desktop-gui/src/runs/runs-list-item.jsx index f95cfe046564..3e94eb050455 100644 --- a/packages/desktop-gui/src/runs/runs-list-item.jsx +++ b/packages/desktop-gui/src/runs/runs-list-item.jsx @@ -2,16 +2,18 @@ import _ from 'lodash' import moment from 'moment' import React, { Component } from 'react' import Tooltip from '@cypress/react-tooltip' +import TimerDisplay from '../duration-timer/TimerDisplay' -import { osIcon, browserIcon, gravatarUrl, getStatusIcon, durationFormatted, browserVersionFormatted } from '../lib/utils' +import { osIcon, browserIcon, gravatarUrl, getStatusIcon, durationFormatted, browserVersionFormatted, stripLeadingCyDirs } from '../lib/utils' export default class RunsListItem extends Component { render () { const run = this.props.run + const NEWLINE = '\n' return ( <li onClick={this._goToRun}> - <div className={`row-column-wrapper ${run.status}`}> + <div className={`row-column-wrapper ${run.status} status-data`}> <div> </div> </div> @@ -26,28 +28,30 @@ export default class RunsListItem extends Component { <div className='row-column-wrapper'> <div className='td-top-padding'> <div> - { - run.commitBranch ? - run.commitBranch : - null + { run.commit ? + <span> + {run.commit.branch ? run.commit.branch : null} + {run.commit.branch && this._displaySpec() ? ' / ' : null} + </span> : + null } + {this._displaySpec()} </div> <div className='msg'> { - run.commitAuthorEmail ? + run.commit && run.commit.authorEmail ? <img className='user-avatar' height='13' width='13' - src={`${gravatarUrl(run.commitAuthorEmail)}`} + src={`${gravatarUrl(run.commit.authorEmail)}`} /> : null } { - run.commitMessage ? + run.commit && run.commit.message ? <span className='commit-msg'> - {' '} - {run.commitMessage.split('\n')[0]} + {run.commit.message.split(NEWLINE)[0]} </span> : null } @@ -68,81 +72,65 @@ export default class RunsListItem extends Component { <i className='fa fa-hourglass-end'></i>{' '} {durationFormatted(run.totalDuration)} </span> : - null + run.createdAt ? + <TimerDisplay startTime={run.createdAt} /> : + null } </div> </div> - <div className='row-column-wrapper'> - <div> + <div className='row-column-wrapper env-data'> + <div className='td-env-padding'> + {/* // do we have multiple OS's ? */} { - // only display something if we have all of the instances back - this._allInstancesArePresent() ? - // do we have multiple OS's ? - this._moreThanOneInstance() && this._osLength() > 1 ? - <span> - <i className='fa fa-fw fa-desktop'></i>{' '} - {this._osLength()} - </span> : - // or did we only actual run it on one OS - <span> - <i className={`fa fa-fw fa-${(this._osIcon())}`}></i>{' '} - {this._osDisplay()} - </span> : + this._instancesExist() ? + this._moreThanOneInstance() && this._osLength() > 1 ? + <div> + <i className='fa 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>{' '} + {this._osDisplay()} + </div> : null } - </div> - </div> - <div className='row-column-wrapper'> - <div> + {/* // do we have multiple browsers ? */} { - // only display something if we have all of the instances back - this._allInstancesArePresent() ? - // do we have multiple browsers ? - this._moreThanOneInstance() && this._browsersLength() > 1 ? - <span> - <i className='fa fa-fw fa-globe'></i>{' '} - {this._browsersLength()} - </span> : - // or did we only actual run it on one browser - <span> - <i className={`fa fa-fw fa-${this._browserIcon()}`}></i>{' '} - {this._browserDisplay()} - </span> : + this._instancesExist() ? + this._moreThanOneInstance() && this._browsersLength() > 1 ? + <div className='msg'> + <i className='fa fa-fw fa-globe'></i>{' '} + {this._browsersLength()} browsers + </div> : + // or did we only actual run it on one browser + <div className='msg'> + <i className={`fa fa-fw fa-${this._browserIcon()}`}></i>{' '} + {this._browserDisplay()} + </div> : null } </div> </div> - <div className='row-column-wrapper'> + <div className='row-column-wrapper passing-data'> { run.status !== 'running' ? <div className='result'> - <i className='fa fa-circle-o-notch'></i>{' '} + <i className='fa fa-check'></i>{' '} <span> - {run.totalPending ? run.totalPending : '-'} + {run.totalPassed || '0'} </span> </div> : null } </div> - <div className='row-column-wrapper'> + <div className='row-column-wrapper failure-data'> { run.status !== 'running' ? <div className='result'> - <i className='fa fa-check green'></i>{' '} + <i className='fa fa-times'></i>{' '} <span> - {run.totalPasses ? run.totalPasses : '-'} - </span> - </div> : - null - } - </div> - <div className='row-column-wrapper'> - { - run.status !== 'running' ? - <div className='result'> - <i className='fa fa-times red'></i>{' '} - <span> - {run.totalFailures ? run.totalFailures : '-'} + {run.totalFailed || '0'} </span> </div> : null @@ -152,43 +140,52 @@ export default class RunsListItem extends Component { ) } - _allInstancesArePresent () { - if (this.props.run.instances) { - return this.props.run.expectedInstances === this.props.run.instances.length - } - } - _moreThanOneInstance () { return (this.props.run.instances.length > 1) } + _instancesExist () { + return (!!this.props.run.instances.length) + } + + _displaySpec () { + // only display spec if we for sure have 1 instance + if (!this._instancesExist() || this._moreThanOneInstance()) return null + + let spec = this.props.run.instances[0].spec + + if (!spec) return null + + return ( + <strong>{stripLeadingCyDirs(spec)}</strong> + ) + } + _getUniqBrowsers () { - if (!this.props.run.instances) return 0 + if (!this.props.run.instances) return [] return _ - .chain(this.props.run.instances) - .map((instance) => { - return `${instance.browserName} + ${instance.browserVersion}` - }) - .uniq() - .value() + .chain(this.props.run.instances) + .map((instance) => { + return `${instance.platform.browserName} + ${instance.platform.browserVersion}` + }) + .uniq() + .value() } _browsersLength () { - return this._getUniqBrowsers().length + if (this._getUniqBrowsers()) { + return this._getUniqBrowsers().length + } } _browserIcon () { - if (!this._moreThanOneInstance() && this.props.run.instances.length) { - return (browserIcon(this.props.run.instances[0].browserName)) - } else { - return 'globe' - } + return browserIcon(this.props.run.instances[0].platform.browserName) } _osIcon () { if (!this._moreThanOneInstance() && this.props.run.instances.length) { - return (osIcon(this.props.run.instances[0].osName)) + return (osIcon(this.props.run.instances[0].platform.osName)) } else { return 'desktop' } @@ -198,12 +195,12 @@ export default class RunsListItem extends Component { if (!this.props.run.instances) return return _ - .chain(this.props.run.instances) - .map((instance) => { - return `${instance.osName} + ${instance.osVersionFormatted}` - }) - .uniq() - .value() + .chain(this.props.run.instances) + .map((instance) => { + return `${instance.platform.osName} + ${instance.platform.osVersionFormatted}` + }) + .uniq() + .value() } _osLength () { @@ -214,7 +211,7 @@ export default class RunsListItem extends Component { if (this.props.run.instances && this.props.run.instances[0]) { return ( <span> - {this.props.run.instances[0].osVersionFormatted} + {this.props.run.instances[0].platform.osVersionFormatted} </span> ) } @@ -224,13 +221,13 @@ export default class RunsListItem extends Component { if (this.props.run.instances && this.props.run.instances[0]) { return ( <span> - {browserVersionFormatted(this.props.run.instances[0].browserVersion)} + {browserVersionFormatted(this.props.run.instances[0].platform.browserVersion)} </span> ) } } _goToRun = () => { - this.props.goToRun(this.props.run.id) + this.props.goToRun(this.props.run.buildNumber) } } diff --git a/packages/desktop-gui/src/runs/runs-list.jsx b/packages/desktop-gui/src/runs/runs-list.jsx index afb22f010751..12808f2241f4 100644 --- a/packages/desktop-gui/src/runs/runs-list.jsx +++ b/packages/desktop-gui/src/runs/runs-list.jsx @@ -200,12 +200,6 @@ class RunsList extends Component { <div className='runs'> <header> <h5>Runs - <a href="#" className='btn btn-sm see-all-runs' onClick={this._openRuns}> - See All <i className='fa fa-external-link'></i> - </a> - - </h5> - <div> {this._lastUpdated()} <button className='btn btn-link btn-sm' @@ -214,6 +208,11 @@ class RunsList extends Component { > <i className={`fa fa-refresh ${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> + </a> </div> </header> <ul className='runs-container list-as-table'> @@ -264,7 +263,7 @@ class RunsList extends Component { _loginMessage () { return ( <div className='empty empty-log-in'> - <h4>Log In to View Runs</h4> + <h4>Log in to view runs</h4> <p> After logging in, you will see recorded runs here and on the <a href='#' onClick={this._visitDashboard}>Cypress Dashboard Service</a>. </p> @@ -377,8 +376,8 @@ class RunsList extends Component { ipc.externalOpen('https://on.cypress.io/what-is-a-project-id') } - _openRun = (runId) => { - ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs/${runId}`) + _openRun = (buildNumber) => { + ipc.externalOpen(`https://on.cypress.io/dashboard/projects/${this.props.project.id}/runs/${buildNumber}`) } _openAPIHelp () { diff --git a/packages/desktop-gui/src/runs/runs.scss b/packages/desktop-gui/src/runs/runs.scss index c9517b6d1254..05e712e38b58 100644 --- a/packages/desktop-gui/src/runs/runs.scss +++ b/packages/desktop-gui/src/runs/runs.scss @@ -102,6 +102,7 @@ font-size: 0.8em; justify-content: space-between; padding-left: 0.8em; + line-height: 40px; h5 { float: left; @@ -110,9 +111,10 @@ } .last-updated { - color: #a3a3a3; + color: #8897a3; + font-size: 12px; margin-left: 1em; - line-height: 39px; + font-weight: normal; } .see-all-runs:active { @@ -148,7 +150,20 @@ .runs-container { margin-bottom: 0; - @include list-columns(0%, 10%, 25%, 14%, 10%, 12%, 7%, 7%, 8%, 7%); + @include list-columns(0%, 10%, 37%, 15%, 10%, 14%, 7%, 7%); + + .status-data { + width: 10%; + + padding-right: 0; + padding-left: 14px; + font-weight: 500; + color: #666; + + &>i { + min-width: 12px; + } + } .user-avatar { position: relative; @@ -165,19 +180,28 @@ display: inline-block; width: 100%; font-size: 12.5px; - color: #898A8B; + color: #738493; + min-height: 1px; &:hover { - background-color: #f8f8f8; + background-color: #f5f8fc; cursor: pointer; + color: #45515b; + box-shadow: 0 1px 2px 0 rgba(0,0,0,.06); } &>.row-column-wrapper>div { - padding: 25px 7px; + padding: 35px 10px; + min-height: 1px; &.td-top-padding { - padding-top: 15px; - padding-bottom: 15px; + padding-top: 20px; + padding-bottom: 20px; + } + + &.td-env-padding { + padding-top: 30px; + padding-bottom: 20px; } } .os-block { @@ -188,6 +212,14 @@ font-weight: 500; } + .passing-data { + color: #1CB77E; + } + + .failure-data { + color: #ec6573; + } + &>.row-column-wrapper:first-child { border-left: 5px solid #eee; min-height: 62px; @@ -248,7 +280,7 @@ .msg { font-weight: 300; - margin-top: 3px; + margin-top: 7px; + img { float: left; @@ -266,7 +298,7 @@ } .user-avatar { - margin-right: 0; + margin-right: 5px; position: relative; top: 0; left: 1px; diff --git a/packages/desktop-gui/src/runs/setup-project-modal.jsx b/packages/desktop-gui/src/runs/setup-project-modal.jsx index 830d8417c7a0..576748777e87 100644 --- a/packages/desktop-gui/src/runs/setup-project-modal.jsx +++ b/packages/desktop-gui/src/runs/setup-project-modal.jsx @@ -82,7 +82,7 @@ class SetupProject extends Component { return ( <div className='setup-project-modal modal-body os-dialog'> <BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss> - <h4>Set Up Project</h4> + <h4>Set up project</h4> <form onSubmit={this._submit}> {this._nameField()} @@ -101,7 +101,7 @@ class SetupProject extends Component { <span><i className='fa fa-spin fa-refresh'></i>{' '}</span> : null } - <span>Set Up Project</span> + <span>Set up project</span> </button> </div> </div> @@ -114,7 +114,7 @@ class SetupProject extends Component { return ( <div className='login modal-body'> <BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss> - <h1><i className='fa fa-lock'></i> Log In</h1> + <h1><i className='fa 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 /> </div> @@ -216,7 +216,7 @@ class SetupProject extends Component { className={cs('btn btn-link', { 'hidden': this.state.owner !== 'org' })} onClick={this._manageOrgs}> <i className='fa fa-plus'></i>{' '} - Create Organization + Create organization </a> </p> </div> @@ -242,7 +242,7 @@ class SetupProject extends Component { value={this.state.orgId || ''} onChange={this._updateOrgId} > - <option value=''>-- Select Organization --</option> + <option value=''>-- Select organization --</option> {_.map(orgsStore.orgs, (org) => { if (org.default) return null diff --git a/packages/desktop-gui/src/specs/spec-model.js b/packages/desktop-gui/src/specs/spec-model.js index ad4e1d87f89a..7953ea7f3bfb 100644 --- a/packages/desktop-gui/src/specs/spec-model.js +++ b/packages/desktop-gui/src/specs/spec-model.js @@ -1,20 +1,22 @@ import { action, observable } from 'mobx' -import { SpecsStore } from './specs-store' export default class Spec { @observable name + @observable displayName + @observable path @observable isChosen = false @observable isExpanded = false - @observable children = new SpecsStore() + @observable children = [] - constructor ({ name, displayName }) { + constructor ({ name, displayName, path }) { this.name = name this.displayName = displayName + this.path = path this.isExpanded = true } hasChildren () { - return this.children.specs && this.children.specs.length; + return this.children && this.children.length } @action setChosen (isChosen) { diff --git a/packages/desktop-gui/src/specs/specs-list.jsx b/packages/desktop-gui/src/specs/specs-list.jsx index 543a3654918b..c2eb98aead25 100644 --- a/packages/desktop-gui/src/specs/specs-list.jsx +++ b/packages/desktop-gui/src/specs/specs-list.jsx @@ -10,78 +10,54 @@ import specsStore from './specs-store' @observer class Specs extends Component { - constructor (props) { - super(props) - this.state = { - search: '', - } - } render () { if (specsStore.isLoading) return <Loader color='#888' scale={0.5}/> - if (!specsStore.specs.length) return this._empty() - - let allActiveClass = specsStore.allSpecsChosen ? 'active' : '' - - const shouldShowClearSearch = this.state.search !== '' + if (!specsStore.filter && !specsStore.specs.length) return this._empty() return ( <div id='tests-list-page'> <header> - <div className="search"> - <label htmlFor="search-input"> - <i className="fa fa-search"></i> + <div className={cs('search', { + 'show-clear-filter': !!specsStore.filter, + })}> + <label htmlFor='filter'> + <i className='fa fa-search'></i> </label> <input - id="search-input" - type="text" - placeholder="Search..." - value={this.state.search} - onChange={this.updateSearchTerms.bind(this)} - onKeyUp={this.executeSearchAction.bind(this)} /> - { - shouldShowClearSearch - ? <a id="clear" className="fa fa-times" onClick={this.clearSearch.bind(this)}/> - : null - } + id='filter' + className='filter' + placeholder='Search...' + value={specsStore.filter || ''} + onChange={this._updateFilter} + onKeyUp={this._executeFilterAction} + /> + <a className='clear-filter fa fa-times' onClick={this._clearFilter} /> </div> - <a onClick={this._selectSpec.bind(this, '__all')} className={`all-tests btn btn-default ${allActiveClass}`}> + <a onClick={this._selectSpec.bind(this, '__all')} className={cs('all-tests btn btn-default', { active: specsStore.allSpecsChosen })}> <i className={`fa fa-fw ${this._allSpecsIcon(specsStore.allSpecsChosen)}`}></i>{' '} - Run All Tests + Run all tests </a> </header> - <ul className='outer-files-container list-as-table'> - {_.map(specsStore.specs, (spec) => this._specItem(spec))} - </ul> + {this._specsList()} </div> ) } - clearSearch () { - this.setState((state) => ({ - ...state, - search: '', - })) - } - - updateSearchTerms (e) { - e.preventDefault() - - const target = e.target - const value = target.value - - this.setState((state) => { - return { - ...state, - search: value, - } - }) - } - - executeSearchAction (e) { - if (e.key === 'Escape') { - this.clearSearch() + _specsList () { + if (specsStore.filter && !specsStore.specs.length) { + return ( + <div className='empty-well'> + No files match the filter '{specsStore.filter}' + </div> + ) } + + return ( + <ul className='outer-files-container list-as-table'> + {_.map(specsStore.specs, (spec) => this._specItem(spec))} + </ul> + ) } _specItem (spec) { @@ -108,6 +84,20 @@ class Specs extends Component { } } + _clearFilter = () => { + specsStore.clearFilter(this.props.project.id) + } + + _updateFilter = (e) => { + specsStore.setFilter(this.props.project.id, e.target.value) + } + + _executeFilterAction = (e) => { + if (e.key === 'Escape') { + this._clearFilter() + } + } + _selectSpec (specPath, e) { e.preventDefault() @@ -139,7 +129,7 @@ class Specs extends Component { isExpanded ? <div> <ul className='list-as-table'> - {_.map(spec.children.specs, (spec) => this._specItem(spec))} + {_.map(spec.children, (spec) => this._specItem(spec))} </ul> </div> : null @@ -151,7 +141,6 @@ class Specs extends Component { _specContent (spec) { const isChosen = specsStore.isChosenSpec(spec) - if (!spec.displayName.toLowerCase().includes(this.state.search.toLowerCase())) return null return ( <li key={spec.path} className='file'> diff --git a/packages/desktop-gui/src/specs/specs-store.js b/packages/desktop-gui/src/specs/specs-store.js index 7526aa2413c7..869d3e7c3d2a 100644 --- a/packages/desktop-gui/src/specs/specs-store.js +++ b/packages/desktop-gui/src/specs/specs-store.js @@ -1,24 +1,30 @@ import _ from 'lodash' import { action, computed, observable } from 'mobx' +import localData from '../lib/local-data' import Spec from './spec-model' export class SpecsStore { - @observable specs = [] + @observable _specs = [] @observable error = null @observable isLoading = false @observable chosenSpecPath + @observable filter = null @computed get allSpecsChosen () { return this.chosenSpecPath === '__all' } + @computed get specs () { + return this._tree(this._specs) + } + @action loading (bool) { this.isLoading = bool } - @action setSpecs (specsByType) { - this.specs = this._tree(specsByType) + @action setSpecs (specs) { + this._specs = specs this.isLoading = false } @@ -31,54 +37,56 @@ export class SpecsStore { spec.setExpanded(!spec.isExpanded) } + @action setFilter (projectId, filter = null) { + localData.set(`specsFilter-${projectId}`, filter) + + this.filter = filter + } + + @action clearFilter (projectId) { + localData.remove(`specsFilter-${projectId}`) + + this.filter = null + } + isChosenSpec (spec) { return spec.name === this.chosenSpecPath || spec.path === this.chosenSpecPath } _tree (specsByType) { - let specsTree = new SpecsStore() - - _.forEach(specsByType, (specs, type) => { - const filesByDirectory = _.map(specs, (spec) => { - // change \\ from Windows to / - let name = spec.name.replace(/\\/g, '/') - return name.split('/') + let specs = _.flatten(_.map(specsByType, (specs, type) => { + return _.map(specs, (spec) => { + // add type (unit, integration, etc) to beginning + // and change \\ to / for Windows + return _.extend({}, spec, { + name: `${type}/${spec.name.replace(/\\/g, '/')}`, + }) }) - _.forEach(filesByDirectory, (segments, index) => { - // add the 'type' to the beginning of the segment - // so it prepend 'unit' or 'integration' - segments.unshift(type) + })) - _.reduce(segments, (memo, segment) => { - // grab the original object - // at index so we can find its path - let specPaths = specs[index] - - // attempt to find an existing spec - // on the specs memo by its segment - let spec = _.find(memo.specs, { displayName: segment }) - - // if its not found then we know we need to - // push a new spec into the specs memo - if (!spec) { - spec = new Spec({ - name: segments.join('/'), - displayName: segment, - }) - - memo.specs.push(spec) - } - - spec.path = specPaths.path - - // and always return the spec's children - return spec.children - - }, specsTree) + if (this.filter) { + specs = _.filter(specs, (spec) => { + return spec.name.toLowerCase().includes(this.filter.toLowerCase()) }) - }) - - return specsTree.specs + } + + return _.reduce(specs, (root, file) => { + let placeholder = root + const segments = file.name.split('/') + _.each(segments, (segment) => { + let spec = _.find(placeholder, { displayName: segment }) + if (!spec) { + spec = new Spec({ + name: file.name, + displayName: segment, + path: file.path, + }) + placeholder.push(spec) + } + placeholder = spec.children + }) + return root + }, []) } } diff --git a/packages/desktop-gui/src/specs/specs.scss b/packages/desktop-gui/src/specs/specs.scss index 2475913aebe8..c42da816048a 100644 --- a/packages/desktop-gui/src/specs/specs.scss +++ b/packages/desktop-gui/src/specs/specs.scss @@ -33,17 +33,23 @@ transform: translate(0, -50%); } - #clear { - text-decoration: none; + .clear-filter { + color: #b3b3b3; + display: none; position: absolute; right: 10px; + text-decoration: none; top: 50%; transform: translate(0, -50%); - color: #b3b3b3; } - input[type=text] { + &.show-clear-filter .clear-filter { + display: block; + } + + .filter { padding-left: 30px; + padding-right: 25px; border-radius: 0; border: 0; border-bottom: 2px solid transparent; @@ -103,12 +109,7 @@ margin-bottom: 0; } - .all-files-container { - margin-top: 60px; - margin-bottom: 40px; - } - - .file>a, .folder { + .file > a, .folder { color: #7c7f84; i { @@ -142,7 +143,7 @@ } } - .file>a { + .file > a { font-weight: 300; border-bottom: 1px dotted #eeeeee; padding: 4px 0; diff --git a/packages/driver/package.json b/packages/driver/package.json index 032f4226ca55..0c28d5694b4e 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -5,8 +5,8 @@ "main": "index.js", "browser": "src/main", "scripts": { - "start": "coffee test/support/server.coffee", - "cypress:open": "node ../../scripts/start.js --project ./test", + "start": "../coffee/node_modules/.bin/coffee test/support/server.coffee", + "cypress:open": "node ../../cli/bin/cypress open --dev --project ./test", "cypress:run": "node ../../scripts/run-cypress-tests.js --browser chrome --dir test", "clean-deps": "rm -rf node_modules" }, diff --git a/packages/driver/src/cy/aliases.coffee b/packages/driver/src/cy/aliases.coffee index 14b09cc7bf86..f50a5c39eba3 100644 --- a/packages/driver/src/cy/aliases.coffee +++ b/packages/driver/src/cy/aliases.coffee @@ -18,6 +18,14 @@ validateAlias = (alias) -> if not _.isString(alias) $utils.throwErrByPath "as.invalid_type" + if aliasDisplayRe.test(alias) + $utils.throwErrByPath "as.invalid_first_token", { + args: { + alias, + suggestedName: alias.replace(aliasDisplayRe, '') + } + } + if _.isBlank(alias) $utils.throwErrByPath "as.empty_string" diff --git a/packages/driver/src/cy/commands/actions/type.coffee b/packages/driver/src/cy/commands/actions/type.coffee index f0cf6d3d1f05..c862cb5c1648 100644 --- a/packages/driver/src/cy/commands/actions/type.coffee +++ b/packages/driver/src/cy/commands/actions/type.coffee @@ -92,7 +92,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> if not isBody and not isTextLike and not hasTabIndex node = $dom.stringify(options.$el) - $utils.throwErrByPath("type.not_on_text_field", { + $utils.throwErrByPath("type.not_on_typeable_element", { onFail: options._log args: { node } }) @@ -369,7 +369,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> }) ## blow up if any member of the subject - ## isnt a textarea or :text + ## isnt a textarea or text-like clear = (el, index) -> $el = $(el) diff --git a/packages/driver/src/cy/commands/exec.coffee b/packages/driver/src/cy/commands/exec.coffee index d14123d122ff..2ceab31940f4 100644 --- a/packages/driver/src/cy/commands/exec.coffee +++ b/packages/driver/src/cy/commands/exec.coffee @@ -52,14 +52,14 @@ module.exports = (Commands, Cypress, cy, state, config) -> args: { cmd, output, code: result.code } } - .catch Promise.TimeoutError, { timedout: true }, (err) -> + .catch Promise.TimeoutError, { timedOut: true }, (err) -> $utils.throwErrByPath "exec.timed_out", { onFail: options._log args: { cmd, timeout: options.timeout } } .catch (error) -> - ## re-throw if timedout error from above + ## re-throw if timedOut error from above throw error if error.name is "CypressError" $utils.throwErrByPath("exec.failed", { diff --git a/packages/driver/src/cy/commands/screenshot.coffee b/packages/driver/src/cy/commands/screenshot.coffee index df5242c8a368..0dca7b9d22f9 100644 --- a/packages/driver/src/cy/commands/screenshot.coffee +++ b/packages/driver/src/cy/commands/screenshot.coffee @@ -1,9 +1,20 @@ _ = require("lodash") Promise = require("bluebird") +$ = require("jquery") +Screenshot = require("../../cypress/screenshot") +$dom = require("../../dom") $utils = require("../../cypress/utils") -takeScreenshot = (runnable, name, log, timeout) -> +getViewportHeight = (state) -> + Math.min(state("viewportHeight"), $(window).height()) + +getViewportWidth = (state) -> + Math.min(state("viewportWidth"), $(window).width()) + +automateScreenshot = (options = {}) -> + { runnable, timeout } = options + titles = [] ## if this a hook then push both the current test title @@ -23,11 +34,10 @@ takeScreenshot = (runnable, name, log, timeout) -> getParentTitle(runnable) - props = { - name: name + props = _.extend({ titles: titles testId: runnable.id - } + }, _.omit(options, "runnable", "timeout", "log", "subject")) automate = -> Cypress.automation("take:screenshot", props) @@ -42,75 +52,305 @@ takeScreenshot = (runnable, name, log, timeout) -> automate() .timeout(timeout) .catch (err) -> - $utils.throwErr(err, { onFail: log }) + $utils.throwErr(err, { onFail: options.log }) .catch Promise.TimeoutError, (err) -> $utils.throwErrByPath "screenshot.timed_out", { - onFail: log - args: { - timeout: timeout - } + onFail: options.log + args: { timeout } } +scrollOverrides = (win, doc) -> + originalOverflow = doc.documentElement.style.overflow + originalBodyOverflowY = doc.body.style.overflowY + originalX = win.scrollX + originalY = win.scrollY + + ## overflow-y: scroll can break `window.scrollTo` + if doc.body + doc.body.style.overflowY = "visible" + + ## hide scrollbars + doc.documentElement.style.overflow = "hidden" + + -> + doc.documentElement.style.overflow = originalOverflow + if doc.body + doc.body.style.overflowY = originalBodyOverflowY + win.scrollTo(originalX, originalY) + +takeScrollingScreenshots = (scrolls, win, automationOptions) -> + scrollAndTake = ({ y, clip, afterScroll }, index) -> + win.scrollTo(0, y) + if afterScroll + clip = afterScroll() + options = _.extend({}, automationOptions, { + current: index + 1 + total: scrolls.length + clip: clip + }) + automateScreenshot(options) + + Promise + .mapSeries(scrolls, scrollAndTake) + .then (results) -> + _.last(results) + +takeFullPageScreenshot = (state, automationOptions) -> + win = state("window") + doc = state("document") + + resetScrollOverrides = scrollOverrides(win, doc) + + docHeight = $(doc).height() + viewportHeight = getViewportHeight(state) + numScreenshots = Math.ceil(docHeight / viewportHeight) + + scrolls = _.map _.times(numScreenshots), (index) -> + y = viewportHeight * index + clip = if index + 1 is numScreenshots + heightLeft = docHeight - (viewportHeight * index) + { + x: automationOptions.clip.x + y: viewportHeight - heightLeft + width: automationOptions.clip.width + height: heightLeft + } + else + automationOptions.clip + + { y, clip } + + takeScrollingScreenshots(scrolls, win, automationOptions) + .finally(resetScrollOverrides) + +takeElementScreenshot = ($el, state, automationOptions) -> + win = state("window") + doc = state("document") + + resetScrollOverrides = scrollOverrides(win, doc) + + elPosition = $dom.getElementPositioning($el) + viewportHeight = getViewportHeight(state) + viewportWidth = getViewportWidth(state) + numScreenshots = Math.ceil(elPosition.height / viewportHeight) + + scrolls = _.map _.times(numScreenshots), (index) -> + y = elPosition.fromWindow.top + (viewportHeight * index) + afterScroll = -> + elPosition = $dom.getElementPositioning($el) + x = Math.min(viewportWidth, elPosition.fromViewport.left) + width = Math.min(viewportWidth - x, elPosition.width) + + if numScreenshots is 1 + return { + x: x + y: elPosition.fromViewport.top + width: width + height: elPosition.height + } + + if index + 1 is numScreenshots + overlap = (numScreenshots - 1) * viewportHeight + elPosition.fromViewport.top + heightLeft = elPosition.fromViewport.bottom - overlap + { + x: x + y: overlap + width: width + height: heightLeft + } + else + { + x: x + y: Math.max(0, elPosition.fromViewport.top) + width: width + ## TODO: try simplifying to just 'viewportHeight' + height: Math.min(viewportHeight, elPosition.fromViewport.top + elPosition.height) + } + + { y, afterScroll } + + takeScrollingScreenshots(scrolls, win, automationOptions) + .finally(resetScrollOverrides) + +## "app only" means we're hiding the runner UI +isAppOnly = ({ capture }) -> + capture is "viewport" or capture is "fullPage" + +getShouldScale = ({ capture, scale }) -> + if isAppOnly({ capture }) then scale else true + +getBlackout = ({ capture, blackout }) -> + if isAppOnly({ capture }) then blackout else [] + +takeScreenshot = (Cypress, state, screenshotConfig, options = {}) -> + { + capture + clip + disableTimersAndAnimations + } = screenshotConfig + + { subject, runnable } = options + + send = (event, props) -> + new Promise (resolve) -> + Cypress.action("cy:#{event}", props, resolve) + + getOptions = (isOpen) -> + { + id: runnable.id + isOpen: isOpen + appOnly: isAppOnly(screenshotConfig) + scale: getShouldScale(screenshotConfig) + waitForCommandSynchronization: not isAppOnly(screenshotConfig) + disableTimersAndAnimations: disableTimersAndAnimations + blackout: getBlackout(screenshotConfig) + } + + before = -> + if disableTimersAndAnimations + cy.pauseTimers(true) + + Screenshot.callBeforeScreenshot(state("document")) + + send("before:screenshot", getOptions(true)) + + after = -> + send("after:screenshot", getOptions(false)) + + Screenshot.callAfterScreenshot(state("document")) + + if disableTimersAndAnimations + cy.pauseTimers(false) + + automationOptions = _.extend({}, options, { + capture: capture + clip: { + x: 0 + y: 0 + width: getViewportWidth(state) + height: getViewportHeight(state) + } + userClip: clip + viewport: { + width: $(window).width() + height: $(window).height() + } + }) + + before() + .then -> + if subject + takeElementScreenshot(subject, state, automationOptions) + else if capture is "fullPage" + takeFullPageScreenshot(state, automationOptions) + else + automateScreenshot(automationOptions) + .finally(after) + module.exports = (Commands, Cypress, cy, state, config) -> + + ## failure screenshot when not interactive Cypress.on "runnable:after:run:async", (test, runnable) -> - ## we want to take a screenshot if we have an error, we're - ## to take a screenshot and we are running from a terminal - ## which means we're exiting at the end - if test.err and config("screenshotOnHeadlessFailure") and config("isTextTerminal") + screenshotConfig = Screenshot.getConfig() + return if not test.err or not screenshotConfig.screenshotOnRunFailure or config("isInteractive") - new Promise (resolve) -> - ## open up our test so we can see it during the screenshot - test.isOpen = true + if not state("screenshotTaken") + ## if a screenshot has not been taken (by cy.screenshot()) in the + ## test that failed, we can bypass UI-changing and pixel-checking + return automateScreenshot({ + capture: "runner" + runnable + simple: true + timeout: config("responseTimeout") + }) - Cypress.action "cy:test:set:state", test, -> - takeScreenshot(runnable) - .then(resolve) + ## if a screenshot has been taken, we need to do all the standard checks + ## to make sure the UI is in the right place + screenshotConfig.capture = "runner" + takeScreenshot(Cypress, state, screenshotConfig, { + runnable + timeout: config("responseTimeout") + }) - Commands.addAll({ - screenshot: (name, options = {}) -> + Commands.addAll({ prevSubject: "optional" }, { + screenshot: (subject, name, userOptions = {}) -> if _.isObject(name) - options = name + userOptions = name name = null + if not $dom.isElement(subject) + subject = null + + withinSubject = state("withinSubject") + if withinSubject and $dom.isElement(withinSubject) + subject = withinSubject + ## TODO: handle hook titles runnable = state("runnable") - _.defaults options, { + options = _.defaults {}, userOptions, { log: true timeout: config("responseTimeout") } - if options.log - consoleProps = {} + screenshotConfig = _.pick(options, "capture", "scale", "disableTimersAndAnimations", "blackout", "waitForCommandSynchronization", "clip") + screenshotConfig = Screenshot.validate(screenshotConfig, "cy.screenshot", options._log) + screenshotConfig = _.extend(Screenshot.getConfig(), screenshotConfig) + ## set this regardless of options.log b/c its used by the + ## yielded value below + consoleProps = _.omit(screenshotConfig, "scale", "screenshotOnRunFailure") + consoleProps = _.extend(consoleProps, { + scaled: getShouldScale(screenshotConfig) + blackout: getBlackout(screenshotConfig) + }) + + if name + consoleProps.name = name + + if options.log options._log = Cypress.log({ message: name consoleProps: -> consoleProps }) - testState = (bool) -> - return { - id: runnable.id - isOpen: bool - } + if subject and subject.length > 1 + $utils.throwErrByPath("screenshot.multiple_elements", { + log: options._log + args: { numElements: subject.length } + }) - setTestState = (bool) -> - new Promise (resolve) -> - ## tell this test to open - Cypress.action("cy:test:set:state", testState(bool), resolve) - - ## open the test for screenshot - setTestState(true) - .then -> - takeScreenshot(runnable, name, options._log, options.timeout) - .finally -> - ## now close the test again no mattter what - setTestState(false) - .then (resp) -> - _.extend consoleProps, { - Saved: resp.path - Size: resp.size - } - .return(null) + if subject + screenshotConfig.capture = "viewport" + + startTime = Date.now() + + state("screenshotTaken", true) + + takeScreenshot(Cypress, state, screenshotConfig, { + subject: subject + runnable: runnable + name: name + log: options._log + timeout: options.timeout + }) + .then (props) -> + duration = Date.now() - startTime + + yieldValue = _.extend({}, consoleProps, props, { duration }) + yieldValue.path = yieldValue.path.replace(/^.*_playground/, '<redacted>') + + { width, height } = props.dimensions + + _.extend(consoleProps, yieldValue, { + duration: "#{duration}ms" + dimensions: "#{width}px x #{height}px" + }) + + if subject + consoleProps.subject = subject + yieldValue.el = subject + + return yieldValue }) diff --git a/packages/driver/src/cy/commands/task.coffee b/packages/driver/src/cy/commands/task.coffee new file mode 100644 index 000000000000..88fad566a035 --- /dev/null +++ b/packages/driver/src/cy/commands/task.coffee @@ -0,0 +1,76 @@ +_ = require("lodash") +Promise = require("bluebird") + +$utils = require("../../cypress/utils") + +module.exports = (Commands, Cypress, cy, state, config) -> + Commands.addAll({ + task: (task, arg, options = {}) -> + _.defaults options, + log: true + timeout: Cypress.config("taskTimeout") + + if options.log + consoleOutput = { + task: task + arg: arg + } + + message = task + if arg + message += ", #{$utils.stringify(arg)}" + + options._log = Cypress.log({ + message: message + consoleProps: -> + consoleOutput + }) + + if not task or not _.isString(task) + $utils.throwErrByPath("task.invalid_argument", { + onFail: options._log, + args: { task: task ? "" } + }) + + ## need to remove the current timeout + ## because we're handling timeouts ourselves + cy.clearTimeout() + + Cypress.backend("task", { + task: task + arg: arg + timeout: options.timeout + }) + .timeout(options.timeout) + .then (result) -> + if options._log + _.extend(consoleOutput, { Yielded: result }) + return result + + .catch Promise.TimeoutError, -> + $utils.throwErrByPath "task.timed_out", { + onFail: options._log + args: { task, timeout: options.timeout } + } + + .catch { timedOut: true }, (error) -> + $utils.throwErrByPath "task.server_timed_out", { + onFail: options._log + args: { task, timeout: options.timeout, error: error.message } + } + + .catch (error) -> + ## re-throw if timedOut error from above + throw error if error.name is "CypressError" + + if error?.isKnownError + $utils.throwErrByPath("task.known_error", { + onFail: options._log + args: { task, error: error.message } + }) + + $utils.throwErrByPath("task.failed", { + onFail: options._log + args: { task, error: error?.stack or error?.message or error } + }) + }) diff --git a/packages/driver/src/cy/commands/window.coffee b/packages/driver/src/cy/commands/window.coffee index a6e3c1ba32c1..18f7c2120cd1 100644 --- a/packages/driver/src/cy/commands/window.coffee +++ b/packages/driver/src/cy/commands/window.coffee @@ -155,7 +155,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> widthAndHeightAreWithinBounds = (width, height) -> _.every [width, height], (val) -> - val >= 200 and val <= 3000 + val >= 20 and val <= 3000 switch when _.isString(presetOrWidth) and _.isBlank(presetOrWidth) diff --git a/packages/driver/src/cy/snapshots.coffee b/packages/driver/src/cy/snapshots.coffee index 45145e5e9f67..c071331e1c89 100644 --- a/packages/driver/src/cy/snapshots.coffee +++ b/packages/driver/src/cy/snapshots.coffee @@ -21,8 +21,17 @@ getCssRulesString = (stylesheet) -> catch e null +screenStylesheetRe = /(screen|all)/ + +isScreenStylesheet = (stylesheet) -> + media = stylesheet.getAttribute("media") + return not _.isString(media) or screenStylesheetRe.test(media) + getStylesFor = (doc, $$, stylesheets, location) -> - _.map $$(location).find("link[rel='stylesheet'],style"), (stylesheet) => + styles = $$(location).find("link[rel='stylesheet'],style") + styles = _.filter(styles, isScreenStylesheet) + + _.map styles, (stylesheet) => ## in cases where we can get the CSS as a string, make the paths ## absolute so that when they're restored by appending them to the page ## in <style> tags, background images and fonts still properly load diff --git a/packages/driver/src/cypress.coffee b/packages/driver/src/cypress.coffee index 3b3c071c37d7..342561c1dc94 100644 --- a/packages/driver/src/cypress.coffee +++ b/packages/driver/src/cypress.coffee @@ -24,6 +24,7 @@ $LocalStorage = require("./cypress/local_storage") $Mocha = require("./cypress/mocha") $Runner = require("./cypress/runner") $Server = require("./cypress/server") +$Screenshot = require("./cypress/screenshot") $SelectorPlayground = require("./cypress/selector_playground") $utils = require("./cypress/utils") @@ -184,7 +185,7 @@ class $Cypress return if @_RESUMED_AT_TEST if @config("isTextTerminal") - @emit("mocha", "start") + @emit("mocha", "start", args[0]) when "runner:end" ## mocha runner has finished running the tests @@ -199,7 +200,7 @@ class $Cypress @emit("run:end") if @config("isTextTerminal") - @emit("mocha", "end") + @emit("mocha", "end", args[0]) when "runner:set:runnable" ## when there is a hook / test (runnable) that @@ -273,8 +274,22 @@ class $Cypress ## stats and runnable properties such as errors @emit("test:after:run", args...) - when "cy:test:set:state" - @emit("test:set:state", args...) + if @config("isTextTerminal") + ## needed for calculating wallClockDuration + ## and the timings of after + afterEach hooks + @emit("mocha", "test:after:run", args[0]) + + when "cy:before:all:screenshots" + @emit("before:all:screenshots", args...) + + when "cy:before:screenshot" + @emit("before:screenshot", args...) + + when "cy:after:screenshot" + @emit("after:screenshot", args...) + + when "cy:after:all:screenshots" + @emit("after:all:screenshots", args...) when "command:log:added" @runner.addLog(args[0], @config("isInteractive")) @@ -450,6 +465,7 @@ class $Cypress Mocha: $Mocha Runner: $Runner Server: $Server + Screenshot: $Screenshot SelectorPlayground: $SelectorPlayground utils: $utils _: _ diff --git a/packages/driver/src/cypress/chai_jquery.coffee b/packages/driver/src/cypress/chai_jquery.coffee index b928471dbac2..774a5ef9cc97 100644 --- a/packages/driver/src/cypress/chai_jquery.coffee +++ b/packages/driver/src/cypress/chai_jquery.coffee @@ -224,7 +224,7 @@ $chaiJquery = (chai, chaiUtils, callbacks = {}) -> assert( @, attr, - actual and actual is val, + actual? and actual is val, message, negatedMessage, val, diff --git a/packages/driver/src/cypress/commands.coffee b/packages/driver/src/cypress/commands.coffee index f360b79dbac7..5746bbe2e836 100644 --- a/packages/driver/src/cypress/commands.coffee +++ b/packages/driver/src/cypress/commands.coffee @@ -31,6 +31,7 @@ builtInCommands = [ require("../cy/commands/querying") require("../cy/commands/request") require("../cy/commands/screenshot") + require("../cy/commands/task") require("../cy/commands/traversals") require("../cy/commands/waiting") require("../cy/commands/window") diff --git a/packages/driver/src/cypress/cy.coffee b/packages/driver/src/cypress/cy.coffee index 5acc3f724b28..ea21084461fa 100644 --- a/packages/driver/src/cypress/cy.coffee +++ b/packages/driver/src/cypress/cy.coffee @@ -141,6 +141,38 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> return ret }) + timersPaused = false + timerQueues = { + setTimeout: [] + setInterval: [] + requestAnimationFrame: [] + } + + runTimerQueue = (queue) -> + _.each timerQueues[queue], ([fn, args, context]) -> + fn.apply(context, args) + timerQueues[queue] = [] + + wrapTimers = (contentWindow) -> + originalSetTimeout = contentWindow.setTimeout + originalSetInterval = contentWindow.setInterval + originalRequestAnimationFrame = contentWindow.requestAnimationFrame + + wrap = (fn, queue) -> (args...) -> + if timersPaused + timerQueues[queue].push([fn, args, this]) + else + fn.apply(this, args) + + contentWindow.setTimeout = (fn, args...) -> + originalSetTimeout(wrap(fn, "setTimeout"), args...) + + contentWindow.setInterval = (fn, args...) -> + originalSetInterval(wrap(fn, "setInterval"), args...) + + contentWindow.requestAnimationFrame = (fn, args...) -> + originalRequestAnimationFrame(wrap(fn, "requestAnimationFrame"), args...) + enqueue = (obj) -> ## if we have a nestedIndex it means we're processing ## nested commands and need to splice them into the @@ -869,6 +901,15 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> contentWindowListeners(contentWindow) + wrapTimers(contentWindow) + + pauseTimers: (pause) -> + timersPaused = pause + if not pause + runTimerQueue("setTimeout") + runTimerQueue("setInterval") + runTimerQueue("requestAnimationFrame") + onSpecWindowUncaughtException: -> ## create the special uncaught exception err err = errors.createUncaughtException("spec", arguments) @@ -1030,7 +1071,6 @@ create = (specWindow, Cypress, Cookies, state, config, log) -> ## but we should still teardown and handle ## the error fail(err, runnable) - } _.each privateProps, (obj, key) => diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index df2c4837f796..ec5dbc9a63db 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -61,6 +61,7 @@ module.exports = { as: empty_string: "#{cmd('as')} cannot be passed an empty string." invalid_type: "#{cmd('as')} can only accept a string." + invalid_first_token: "'{{alias}}' cannot be named starting with the '@' symbol. Try renaming the alias to '{{suggestedName}}', or something else that does not start with the '@' symbol." reserved_word: "#{cmd('as')} cannot be aliased as: '{{alias}}'. This word is reserved." blur: @@ -100,7 +101,15 @@ module.exports = { invalid_element: "#{cmd('{{cmd}}')} can only be called on :checkbox{{phrase}}. Your subject {{word}} a: {{node}}" clear: - invalid_element: "#{cmd('clear')} can only be called on textarea or :text. Your subject {{word}} a: {{node}}" + invalid_element: """ + #{cmd('clear')} failed because it requires a valid clearable element. + + The element cleared was: + + > {{node}} + + Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements. + """ clearCookie: invalid_argument: "#{cmd('clearCookie')} must be passed a string argument for name." @@ -627,6 +636,13 @@ module.exports = { animation_failed: "#{cmd('scrollTo')} failed." screenshot: + invalid_arg: "{{cmd}}() must be called with an object. You passed: {{arg}}" + invalid_capture: "{{cmd}}() 'capture' option must be one of the following: 'fullPage', 'viewport', or 'runner'. You passed: {{arg}}" + invalid_boolean: "{{cmd}}() '{{option}}' option must be a boolean. You passed: {{arg}}" + invalid_blackout: "{{cmd}}() 'blackout' option must be an array of strings. You passed: {{arg}}" + invalid_clip: "{{cmd}}() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: {{arg}}" + invalid_callback: "{{cmd}}() '{{callback}}' option must be a function. You passed: {{arg}}" + multiple_elements: "#{cmd('screenshot')} only works for a single element. You attempted to screenshot {{numElements}} elements." timed_out: "#{cmd('screenshot')} timed out waiting '{{timeout}}ms' to complete." select: @@ -719,6 +735,22 @@ module.exports = { multiple_forms: "#{cmd('submit')} can only be called on a single form. Your subject contained {{num}} form elements." not_on_form: "#{cmd('submit')} can only be called on a <form>. Your subject {{word}} a: {{node}}" + task: + known_error: """#{cmd('task', '\'{{task}}\'')} failed with the following error: + + {{error}} + """ + failed: """#{cmd('task', '\'{{task}}\'')} failed with the following error: + + > {{error}} + """ + invalid_argument: "#{cmd('task')} must be passed a non-empty string as its 1st argument. You passed: '{{task}}'." + timed_out: "#{cmd('task', '\'{{task}}\'')} timed out after waiting {{timeout}}ms." + server_timed_out: """#{cmd('task', '\'{{task}}\'')} timed out after waiting {{timeout}}ms. + + {{error}} + """ + tick: invalid_argument: "clock.tick()/#{cmd('tick')} only accept a number as their argument. You passed: {{arg}}" no_clock: "#{cmd('tick')} cannot be called without first calling #{cmd('clock')}" @@ -747,8 +779,16 @@ module.exports = { invalid_month: "Typing into a month input with #{cmd('type')} requires a valid month with the format 'yyyy-MM'. You passed: {{chars}}" invalid_week: "Typing into a week input with #{cmd('type')} requires a valid week with the format 'yyyy-Www', where W is the literal character 'W' and ww is the week number (00-53). You passed: {{chars}}" invalid_time: "Typing into a time input with #{cmd('type')} requires a valid time with the format 'HH:mm', 'HH:mm:ss' or 'HH:mm:ss.SSS', where HH is 00-23, mm is 00-59, ss is 00-59, and SSS is 000-999. You passed: {{chars}}" - multiple_elements: "#{cmd('type')} can only be called on a single textarea or :text. Your subject contained {{num}} elements." - not_on_text_field: "#{cmd('type')} can only be called on textarea or :text. Your subject is a: {{node}}" + multiple_elements: "#{cmd('type')} can only be called on a single element. Your subject contained {{num}} elements." + not_on_typeable_element: """ + #{cmd('type')} failed because it requires a valid typeable element. + + The element typed into was: + + > {{node}} + + Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements. + """ tab: "{tab} isn't a supported character sequence. You'll want to use the command #{cmd('tab')}, which is not ready yet, but when it is done that's what you'll use." wrong_type: "#{cmd('type')} can only accept a String or Number. You passed in: '{{chars}}'" @@ -799,7 +839,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 200px and 3000px." + dimensions_out_of_range: "#{cmd('viewport')} width and height must be between 20px and 3000px." 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}}" diff --git a/packages/driver/src/cypress/runner.coffee b/packages/driver/src/cypress/runner.coffee index 8c583ea9ead6..9f7e9f30af0e 100644 --- a/packages/driver/src/cypress/runner.coffee +++ b/packages/driver/src/cypress/runner.coffee @@ -15,7 +15,7 @@ TEST_AFTER_RUN_EVENT = "runner:test:after:run" ERROR_PROPS = "message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff".split(" ") RUNNABLE_LOGS = "routes agents commands".split(" ") -RUNNABLE_PROPS = "id title root hookName err duration state failedFromHook body".split(" ") +RUNNABLE_PROPS = "id title root hookName hookId err state failedFromHookId body speed type duration wallClockStartedAt wallClockDuration timings".split(" ") # ## initial payload # { @@ -87,6 +87,7 @@ runnableAfterRunAsync = (runnable, Cypress) -> testAfterRun = (test, Cypress) -> if not fired(TEST_AFTER_RUN_EVENT, test) + setWallClockDuration(test) fire(TEST_AFTER_RUN_EVENT, test, Cypress) ## perf loop only through @@ -108,6 +109,18 @@ testAfterRun = (test, Cypress) -> ## prevent loop comprehension return null +setTestTimingsForHook = (test, hookName, obj) -> + test.timings ?= {} + test.timings[hookName] ?= [] + test.timings[hookName].push(obj) + +setTestTimings = (test, name, obj) -> + test.timings ?= {} + test.timings[name] = obj + +setWallClockDuration = (test) -> + test.wallClockDuration = new Date() - test.wallClockStartedAt + reduceProps = (obj, props) -> _.reduce props, (memo, prop) -> if _.has(obj, prop) or (obj[prop] isnt undefined) @@ -239,7 +252,7 @@ isLastSuite = (suite, tests) -> ## if we failed from a hook and that hook was 'before' ## since then mocha skips the remaining tests in the suite lastTestThatWillRunInSuite = (test, tests) -> - isLastTest(test, tests) or (test.failedFromHook and test.hookName is "before all") + isLastTest(test, tests) or (test.failedFromHookId and test.hookName is "before all") isLastTest = (test, tests) -> test is _.last(tests) @@ -333,7 +346,7 @@ getTestResults = (tests) -> obj.state = "skipped" obj -normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getId) -> +normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnable, onLogsById, getTestId) -> hasTests = false ## only loop until we find the first test @@ -350,7 +363,7 @@ normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnab tests = {} grepIsDefault = _.isEqual(grep, defaultGrepRe) - obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getId) + obj = normalize(suite, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) if setTestsById ## use callback here to hand back @@ -363,9 +376,9 @@ normalizeAll = (suite, initialTests = {}, grep, setTestsById, setTests, onRunnab return obj -normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getId) -> +normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) -> normalizer = (runnable) => - runnable.id = getId() + runnable.id = getTestId() ## tests have a type of 'test' whereas suites do not have a type property runnable.type ?= "suite" @@ -401,7 +414,7 @@ normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onL _.each {tests: runnable.tests, suites: runnable.suites}, (_runnables, key) => if runnable[key] obj[key] = _.map _runnables, (runnable) => - normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getId) + normalize(runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onLogsById, getTestId) else ## iterate through all tests and only push them in ## if they match the current grep @@ -431,7 +444,7 @@ normalize = (runnable, tests, initialTests, grep, grepIsDefault, onRunnable, onL grepIsDefault, onRunnable, onLogsById, - getId + getTestId ) ) @@ -453,9 +466,9 @@ hookFailed = (hook, err, hookName, getTestById, getTest) -> test = getTest() or getTestFromHook(hook, hook.parent, getTestById) test.err = err test.state = "failed" - test.duration = hook.duration - test.hookName = hookName - test.failedFromHook = true + test.duration = hook.duration ## TODO: nope (?) + test.hookName = hookName ## TODO: why are we doing this? + test.failedFromHookId = hook.hookId if hook.alreadyEmittedMocha ## TODO: won't this always hit right here??? @@ -464,12 +477,16 @@ hookFailed = (hook, err, hookName, getTestById, getTest) -> else Cypress.action("runner:test:end", wrap(test)) -_runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, setTest) -> +_runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) -> _runner.on "start", -> - Cypress.action("runner:start") + Cypress.action("runner:start", { + start: new Date() + }) _runner.on "end", -> - Cypress.action("runner:end") + Cypress.action("runner:end", { + end: new Date() + }) _runner.on "suite", (suite) -> return if _emissions.started[suite.id] @@ -490,13 +507,14 @@ _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, setTest) Cypress.action("runner:suite:end", wrap(suite)) _runner.on "hook", (hook) -> - hookName = getHookName(hook) + hook.hookId ?= getHookId() + hook.hookName ?= getHookName(hook) ## mocha incorrectly sets currentTest on before all's. ## if there is a nested suite with a before, then ## currentTest will refer to the previous test run ## and not our current - if hookName is "before all" and hook.ctx.currentTest + if hook.hookName is "before all" and hook.ctx.currentTest delete hook.ctx.currentTest ## set the hook's id from the test because @@ -515,8 +533,6 @@ _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, setTest) Cypress.action("runner:hook:start", wrap(hook)) _runner.on "hook end", (hook) -> - hookName = getHookName(hook) - Cypress.action("runner:hook:end", wrap(hook)) _runner.on "test", (test) -> @@ -608,6 +624,7 @@ _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, setTest) create = (specWindow, mocha, Cypress, cy) -> _id = 0 + _hookId = 0 _uncaughtFn = null _runner = mocha.getRunner() @@ -662,10 +679,13 @@ create = (specWindow, mocha, Cypress, cy) -> } _startTime = null - getId = -> + getTestId = -> ## increment the id counter "r" + (_id += 1) + getHookId = -> + "h" + (_hookId += 1) + setTestsById = (tbid) -> _testsById = tbid @@ -734,13 +754,13 @@ create = (specWindow, mocha, Cypress, cy) -> setTests, onRunnable, onLogsById, - getId + getTestId ) run: (fn) -> _startTime ?= moment().toJSON() - _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest) + _runnerListeners(_runner, Cypress, _emissions, getTestById, getTest, setTest, getHookId) _runner.run (failures) -> ## if we happen to make it all the way through @@ -757,15 +777,37 @@ create = (specWindow, mocha, Cypress, cy) -> if not runnable.id throw new Error("runnable must have an id", runnable.id) - ## if this isnt a hook, then the name is 'test' - hookName = getHookName(runnable) or "test" - switch runnable.type when "hook" test = getTest() or getTestFromHook(runnable, runnable.parent, getTestById) + when "test" test = runnable + ## closure for calculating the actual + ## runtime of a runnables fn exection duration + ## and also the run of the runnable:after:run:async event + wallClockStartedAt = null + wallClockEnd = null + fnDurationStart = null + fnDurationEnd = null + afterFnDurationStart = null + afterFnDurationEnd = null + + ## when this is a hook, capture the real start + ## date so we can calculate our test's duration + ## including all of its hooks + wallClockStartedAt = new Date() + + if not test.wallClockStartedAt + ## if we don't have lifecycle timings yet + lifecycleStart = wallClockStartedAt + + test.wallClockStartedAt ?= wallClockStartedAt + + ## if this isnt a hook, then the name is 'test' + hookName = getHookName(runnable) or "test" + ## if we haven't yet fired this event for this test ## that means that we need to reset the previous state ## of cy - since we now have a new 'test' and all of the @@ -775,16 +817,47 @@ create = (specWindow, mocha, Cypress, cy) -> ## extract out the next(fn) which mocha uses to ## move to the next runnable - this will be our async seam - next = args[0] - - ## our runnable is about to run, so let cy know. this enables - ## us to always have a correct runnable set even when we are - ## running lifecycle events - ## and also get back a function result handler that we use as - ## an async seam - cy.setRunnable(runnable, hookName) + _next = args[0] + + next = (err) -> + ## now set the duration of the after runnable run async event + afterFnDurationEnd = wallClockEnd = new Date() + + switch runnable.type + when "hook" + ## reset runnable duration to include lifecycle + ## and afterFn timings purely for the mocha runner. + ## this is what it 'feels' like to the user + runnable.duration = wallClockEnd - wallClockStartedAt + + setTestTimingsForHook(test, hookName, { + hookId: runnable.hookId + fnDuration: fnDurationEnd - fnDurationStart + afterFnDuration: afterFnDurationEnd - afterFnDurationStart + }) + + when "test" + ## if we are currently on a test then + ## recalculate its duration to be based + ## against that (purely for the mocha reporter) + test.duration = wallClockEnd - test.wallClockStartedAt + + ## but still preserve its actual function + ## body duration for timings + setTestTimings(test, "test", { + fnDuration: fnDurationEnd - fnDurationStart + afterFnDuration: afterFnDurationEnd - afterFnDurationStart + }) + + _next(err) onNext = (err) -> + ## when done with the function set that to end + fnDurationEnd = new Date() + + ## and also set the afterFnDuration to this same date + afterFnDurationStart = fnDurationEnd + ## attach error right now ## if we have one if err @@ -813,6 +886,13 @@ create = (specWindow, mocha, Cypress, cy) -> ## the test.run(fn) return null + ## our runnable is about to run, so let cy know. this enables + ## us to always have a correct runnable set even when we are + ## running lifecycle events + ## and also get back a function result handler that we use as + ## an async seam + cy.setRunnable(runnable, hookName) + ## TODO: handle promise timeouts here! ## whenever any runnable is about to run ## we figure out what test its associated to @@ -835,6 +915,15 @@ create = (specWindow, mocha, Cypress, cy) -> throw err .finally -> + if lifecycleStart + ## capture how long the lifecycle took as part + ## of the overall wallClockDuration of our test + setTestTimings(test, "lifecycle", new Date() - lifecycleStart) + + ## capture the moment we're about to invoke + ## the runnable's callback function + fnDurationStart = new Date() + ## call the original method with our ## custom onNext function runnableRun.call(runnable, onNext) diff --git a/packages/driver/src/cypress/screenshot.coffee b/packages/driver/src/cypress/screenshot.coffee new file mode 100644 index 000000000000..e4d649ae7c2f --- /dev/null +++ b/packages/driver/src/cypress/screenshot.coffee @@ -0,0 +1,122 @@ +_ = require("lodash") + +$utils = require("./utils") + +reset = -> { + capture: "fullPage" + scale: false + disableTimersAndAnimations: true + screenshotOnRunFailure: true + blackout: [] + beforeScreenshot: -> + afterScreenshot: -> +} + +defaults = reset() + +validCaptures = ["fullPage", "viewport", "runner"] + +validateAndSetBoolean = (props, values, cmd, log, option) -> + value = props[option] + if not value? + return + + if not _.isBoolean(value) + $utils.throwErrByPath("screenshot.invalid_boolean", { + log: log + args: { + cmd: cmd + option: option + arg: $utils.stringify(value) + } + }) + + values[option] = value + +validateAndSetCallback = (props, values, cmd, log, option) -> + value = props[option] + if not value? + return + + if not _.isFunction(value) + $utils.throwErrByPath("screenshot.invalid_callback", { + log: log + args: { + cmd: cmd + callback: option + arg: $utils.stringify(value) + } + }) + + values[option] = value + +validate = (props, cmd, log) -> + values = {} + + if not _.isPlainObject(props) + $utils.throwErrByPath("screenshot.invalid_arg", { + log: log + args: { cmd: cmd, arg: $utils.stringify(props) } + }) + + if capture = props.capture + if not (capture in validCaptures) + $utils.throwErrByPath("screenshot.invalid_capture", { + log: log + args: { cmd: cmd, arg: $utils.stringify(capture) } + }) + + values.capture = capture + + validateAndSetBoolean(props, values, cmd, log, "scale") + validateAndSetBoolean(props, values, cmd, log, "disableTimersAndAnimations") + validateAndSetBoolean(props, values, cmd, log, "screenshotOnRunFailure") + + if blackout = props.blackout + if not _.isArray(blackout) or _.some(blackout, (selector) -> not _.isString(selector)) + $utils.throwErrByPath("screenshot.invalid_blackout", { + log: log + args: { cmd: cmd, arg: $utils.stringify(blackout) } + }) + + values.blackout = blackout + + if clip = props.clip + if ( + not _.isPlainObject(clip) or + _.some(clip, (value) -> not _.isNumber(value)) or + _.sortBy(_.keys(clip)).join(",") isnt "height,width,x,y" + ) + $utils.throwErrByPath("screenshot.invalid_clip", { + log: log + args: { cmd: cmd, arg: $utils.stringify(clip) } + }) + + values.clip = clip + + validateAndSetCallback(props, values, cmd, log, "beforeScreenshot") + validateAndSetCallback(props, values, cmd, log, "afterScreenshot") + + return values + +module.exports = { + reset: -> + ## for testing purposes + defaults = reset() + + getConfig: -> + _.cloneDeep(_.omit(defaults, "beforeScreenshot", "afterScreenshot")) + + callBeforeScreenshot: (doc) -> + defaults.beforeScreenshot(doc) + + callAfterScreenshot: (doc) -> + defaults.afterScreenshot(doc) + + defaults: (props) -> + values = validate(props, "Cypress.Screenshot.defaults") + _.extend(defaults, values) + + validate: validate + } + diff --git a/packages/driver/src/cypress/utils.coffee b/packages/driver/src/cypress/utils.coffee index 13a5820608ce..663767d7ae06 100644 --- a/packages/driver/src/cypress/utils.coffee +++ b/packages/driver/src/cypress/utils.coffee @@ -1,6 +1,7 @@ $ = require("jquery") _ = require("lodash") moment = require("moment") +Promise = require("bluebird") $Location = require("./location") $errorMessages = require("./error_messages") @@ -292,4 +293,21 @@ module.exports = { deltaY = point1.y - point2.y Math.sqrt(deltaX * deltaX + deltaY * deltaY) + + runSerially: (fns) -> + values = [] + + run = (index) -> + Promise + .try -> + fns[index]() + .then (value) -> + values.push(value) + index++ + if fns[index] + run(index) + else + values + + run(0) } diff --git a/packages/driver/test/cypress.json b/packages/driver/test/cypress.json index db036b1f69b9..4fefa431dad2 100644 --- a/packages/driver/test/cypress.json +++ b/packages/driver/test/cypress.json @@ -2,6 +2,5 @@ "baseUrl": "http://localhost:3500", "hosts": { "*.foobar.com": "127.0.0.1" - }, - "pluginsFile": false + } } diff --git a/packages/driver/test/cypress/.eslintrc b/packages/driver/test/cypress/.eslintrc new file mode 100644 index 000000000000..5b988562725d --- /dev/null +++ b/packages/driver/test/cypress/.eslintrc @@ -0,0 +1,8 @@ +{ + "plugins": [ + "cypress" + ], + "env": { + "cypress/globals": true + } +} diff --git a/packages/driver/test/cypress/fixtures/issue-1436.html b/packages/driver/test/cypress/fixtures/issue-1436.html new file mode 100644 index 000000000000..5444e6a8d9a2 --- /dev/null +++ b/packages/driver/test/cypress/fixtures/issue-1436.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> + <head> + <title>Issue 1436 + + + + + diff --git a/packages/driver/test/cypress/fixtures/screenshots.html b/packages/driver/test/cypress/fixtures/screenshots.html new file mode 100644 index 000000000000..7fcd2c413f6f --- /dev/null +++ b/packages/driver/test/cypress/fixtures/screenshots.html @@ -0,0 +1,38 @@ + + + + Screenshots Fixture + + + +
+
+
+
+
+
+ + diff --git a/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee index 7b993ea9cd9a..a81d6e7bb8e1 100644 --- a/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/trigger_spec.coffee @@ -49,11 +49,20 @@ describe "src/cy/commands/actions/trigger", -> .then (win) -> $win = $(win) - $win.on "mouseover", -> - done(new Error("should not have bubbled up to window listener")) - - cy.get("#button").trigger("mouseover", {bubbles: false}).then -> - $win.off "mouseover" + $win.on "keydown", (e) -> + evt = JSON.stringify(e.originalEvent, [ + "bubbles", "cancelable", "isTrusted", "type", "clientX", "clientY" + ]) + + done(new Error("event should not have bubbled up to window listener: #{evt}")) + + cy + .get("#button") + .trigger("keydown", { + bubbles: false + }) + .then -> + $win.off "keydown" done() diff --git a/packages/driver/test/cypress/integration/commands/actions/type_spec.coffee b/packages/driver/test/cypress/integration/commands/actions/type_spec.coffee index da34291df6b1..4d9dac6ddb8b 100644 --- a/packages/driver/test/cypress/integration/commands/actions/type_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/actions/type_spec.coffee @@ -2158,22 +2158,24 @@ describe "src/cy/commands/actions/type", -> cy.get("input:first").type("a").type("b") - it "throws when not textarea or :text", (done) -> + it "throws when not textarea or text-like", (done) -> cy.get("form").type("foo") cy.on "fail", (err) -> - expect(err.message).to.include "cy.type() can only be called on textarea or :text. Your subject is a:
...
" + expect(err.message).to.include "cy.type() failed because it requires a valid typeable element." + expect(err.message).to.include "The element typed into was:" + expect(err.message).to.include "
...
" + expect(err.message).to.include "Cypress considers the 'body', 'textarea', any 'element' with a 'tabindex' or 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid typeable elements." done() it "throws when subject is a collection of elements", (done) -> - cy - .get("textarea,:text").then ($inputs) -> + cy.get("textarea,:text").then ($inputs) -> @num = $inputs.length return $inputs .type("foo") cy.on "fail", (err) => - expect(err.message).to.include "cy.type() can only be called on a single textarea or :text. Your subject contained #{@num} elements." + expect(err.message).to.include "cy.type() can only be called on a single element. Your subject contained #{@num} elements." done() it "throws when the subject isnt visible", (done) -> @@ -2570,34 +2572,46 @@ describe "src/cy/commands/actions/type", -> cy.get("input:first").clear().clear() - it "throws if any subject isnt a textarea", (done) -> + it "throws if any subject isnt a textarea or text-like", (done) -> cy.on "fail", (err) => lastLog = @lastLog expect(@logs.length).to.eq(3) expect(lastLog.get("error")).to.eq(err) - expect(err.message).to.include "cy.clear() can only be called on textarea or :text. Your subject contains a:
...
" + expect(err.message).to.include "cy.clear() failed because it requires a valid clearable element." + expect(err.message).to.include "The element cleared was:" + expect(err.message).to.include "
...
" + expect(err.message).to.include "Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements." done() cy.get("textarea:first,form#checkboxes").clear() it "throws if any subject isnt a :text", (done) -> cy.on "fail", (err) -> - expect(err.message).to.include "cy.clear() can only be called on textarea or :text. Your subject contains a:
...
" + expect(err.message).to.include "cy.clear() failed because it requires a valid clearable element." + expect(err.message).to.include "The element cleared was:" + expect(err.message).to.include "
...
" + expect(err.message).to.include "Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements." done() cy.get("div").clear() it "throws on an input radio", (done) -> cy.on "fail", (err) -> - expect(err.message).to.include "cy.clear() can only be called on textarea or :text. Your subject contains a: " + expect(err.message).to.include "cy.clear() failed because it requires a valid clearable element." + expect(err.message).to.include "The element cleared was:" + expect(err.message).to.include "" + expect(err.message).to.include "Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements." done() cy.get(":radio").clear() it "throws on an input checkbox", (done) -> cy.on "fail", (err) -> - expect(err.message).to.include "cy.clear() can only be called on textarea or :text. Your subject contains a: " + expect(err.message).to.include "cy.clear() failed because it requires a valid clearable element." + expect(err.message).to.include "The element cleared was:" + expect(err.message).to.include "" + expect(err.message).to.include "Cypress considers a 'textarea', any 'element' with a 'contenteditable' attribute, or any 'input' with a 'type' attribute of 'text', 'password', 'email', 'number', 'date', 'week', 'month', 'time', 'datetime', 'datetime-local', 'search', 'url', or 'tel' to be valid clearable elements." done() cy.get(":checkbox").clear() diff --git a/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee b/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee index 219fa3e7ff47..9866e05398fe 100644 --- a/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/aliasing_spec.coffee @@ -123,6 +123,17 @@ describe "src/cy/commands/aliasing", -> cy.get("div:first").as("") + it "throws on alias starting with @ char", (done) -> + cy.on "fail", (err) -> + expect(err.message).to.eq "'@myAlias' cannot be named starting with the '@' symbol. Try renaming the alias to 'myAlias', or something else that does not start with the '@' symbol." + done() + + cy.get("div:first").as("@myAlias") + + it "does not throw on alias with @ char in non-starting position", () -> + cy.get("div:first").as("my@Alias") + cy.get("@my@Alias") + _.each ["test", "runnable", "timeout", "slow", "skip", "inspect"], (blacklist) -> it "throws on a blacklisted word: #{blacklist}", (done) -> cy.on "fail", (err) -> diff --git a/packages/driver/test/cypress/integration/commands/assertions_spec.coffee b/packages/driver/test/cypress/integration/commands/assertions_spec.coffee index 7c5bb696a1e4..246d11351c5d 100644 --- a/packages/driver/test/cypress/integration/commands/assertions_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/assertions_spec.coffee @@ -657,6 +657,18 @@ describe "src/cy/commands/assertions", -> cy.get("body").then ($body) -> expect($body).to.exist + it "matches empty string attributes", (done) -> + cy.on "log:added", (attrs, log) => + if attrs.name is "assert" + cy.removeAllListeners("log:added") + + expect(log.get("message")).to.eq "expected **** to have attribute **value** with the value **''**" + done() + + cy.$$("body").prepend $("") + cy.get("input").eq(0).then ($input) -> + expect($input).to.have.attr('value', '') + describe "without selector", -> it "exists", (done) -> cy.on "log:added", (attrs, log) => diff --git a/packages/driver/test/cypress/integration/commands/exec_spec.coffee b/packages/driver/test/cypress/integration/commands/exec_spec.coffee index d1ea3edced70..308c89338f99 100644 --- a/packages/driver/test/cypress/integration/commands/exec_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/exec_spec.coffee @@ -190,7 +190,7 @@ describe "src/cy/commands/exec", -> it "can timeout from the backend's response", (done) -> err = new Error("timeout") - err.timedout = true + err.timedOut = true Cypress.backend.rejects(err) diff --git a/packages/driver/test/cypress/integration/commands/querying_spec.coffee b/packages/driver/test/cypress/integration/commands/querying_spec.coffee index e89f21634c4a..633464bdf00a 100644 --- a/packages/driver/test/cypress/integration/commands/querying_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/querying_spec.coffee @@ -769,7 +769,7 @@ describe "src/cy/commands/querying", -> .visit("http://localhost:3500/fixtures/jquery.html") .server() .route(/users/, {}).as("getUsers") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> win.$.get("/users") .get("@getUsers").then -> expect(@lastLog.pick("message", "referencesAlias", "aliasType")).to.deep.eq { @@ -859,7 +859,7 @@ describe "src/cy/commands/querying", -> .server() .route(/users/, {}).as("getUsers") .visit("http://localhost:3500/fixtures/jquery.html") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> win.$.get("/users") .get("@getUsers").then (obj) -> expect(@lastLog.invoke("consoleProps")).to.deep.eq { @@ -870,7 +870,7 @@ describe "src/cy/commands/querying", -> describe "alias references", -> beforeEach -> - Cypress.config("defaultCommandTimeout", 200) + Cypress.config("defaultCommandTimeout", 100) it "can get alias primitives", -> cy @@ -913,7 +913,7 @@ describe "src/cy/commands/querying", -> .server() .route(/users/, {}).as("getUsers") .visit("http://localhost:3500/fixtures/jquery.html") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> win.$.get("/users") .get("@getUsers").then (xhr) -> expect(xhr.url).to.include "/users" @@ -931,7 +931,7 @@ describe "src/cy/commands/querying", -> .visit("http://localhost:3500/fixtures/jquery.html") .server() .route(/users/, {}).as("getUsers") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> Promise.all([ win.$.get("/users", {num: 1}) win.$.get("/users", {num: 2}) @@ -946,7 +946,7 @@ describe "src/cy/commands/querying", -> .visit("http://localhost:3500/fixtures/jquery.html") .server() .route(/users/, {}).as("getUsers") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> Promise.all([ win.$.get("/users", {num: 1}) win.$.get("/users", {num: 2}) @@ -959,7 +959,7 @@ describe "src/cy/commands/querying", -> .visit("http://localhost:3500/fixtures/jquery.html") .server() .route(/users/, {}).as("getUsers") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> Promise.all([ win.$.get("/users", {num: 1}) win.$.get("/users", {num: 2}) @@ -972,7 +972,7 @@ describe "src/cy/commands/querying", -> .server() .route(/users/, {}).as("getUsers") .visit("http://localhost:3500/fixtures/jquery.html") - .window().then (win) -> + .window().then { timeout: 2000 }, (win) -> Promise.all([ win.$.get("/users", {num: 1}) win.$.get("/users", {num: 2}) diff --git a/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee b/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee index 59debae2e6b9..088151b8c95f 100644 --- a/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/screenshot_spec.coffee @@ -1,10 +1,30 @@ +$ = require("jquery") + _ = Cypress._ Promise = Cypress.Promise +Screenshot = Cypress.Screenshot describe "src/cy/commands/screenshot", -> beforeEach -> cy.stub(Cypress, "automation").callThrough() + @serverResult = { + path: "/path/to/screenshot" + size: "12 B" + dimensions: { width: 20, height: 20 } + multipart: false + pixelRatio: 1 + takenAt: new Date().toISOString() + } + + @screenshotConfig = { + capture: "viewport" + screenshotOnRunFailure: true + disableTimersAndAnimations: true + scale: true + blackout: [".foo"] + } + context "runnable:after:run:async", -> it "is noop when not isTextTerminal", -> Cypress.config("isTextTerminal", false) @@ -36,9 +56,11 @@ describe "src/cy/commands/screenshot", -> expect(Cypress.action).not.to.be.calledWith("cy:test:set:state") expect(Cypress.automation).not.to.be.called - it "is noop when screenshotOnHeadlessFailure is false", -> + it "is noop when screenshotOnRunFailure is false", -> Cypress.config("isInteractive", false) - Cypress.config("screenshotOnHeadlessFailure", false) + cy.stub(Screenshot, "getConfig").returns({ + screenshotOnRunFailure: false + }) cy.spy(Cypress, "action").log(false) @@ -53,17 +75,36 @@ describe "src/cy/commands/screenshot", -> expect(Cypress.action).not.to.be.calledWith("cy:test:set:state") expect(Cypress.automation).not.to.be.called - it "sets test state and takes screenshot when not isInteractive", -> + it "does not send before/after events", -> Cypress.config("isInteractive", false) + @screenshotConfig.scale = false + cy.stub(Screenshot, "getConfig").returns(@screenshotConfig) - Cypress.automation.withArgs("take:screenshot").resolves({}) + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) cy.stub(Cypress, "action").log(false) .callThrough() - .withArgs("cy:test:set:state") + .withArgs("cy:before:screenshot") + .yieldsAsync() + .withArgs("cy:after:screenshot") .yieldsAsync() + test = { id: "123", err: new Error() } + runnable = cy.state("runnable") + + Cypress.action("runner:runnable:after:run:async", test, runnable) + .then -> + expect(Cypress.action).not.to.be.calledWith("cy:before:screenshot") + expect(Cypress.action).not.to.be.calledWith("cy:after:screenshot") + + it "takes screenshot when not isInteractive", -> + Cypress.config("isInteractive", false) + cy.stub(Screenshot, "getConfig").returns(@screenshotConfig) + + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + test = { + id: "123" err: new Error } @@ -71,45 +112,106 @@ describe "src/cy/commands/screenshot", -> Cypress.action("runner:runnable:after:run:async", test, runnable) .then -> - ## this should mutate this property - expect(test.isOpen).to.be.true - - expect(Cypress.action).to.be.calledWith("cy:test:set:state", test) - expect(Cypress.automation).to.be.calledWith("take:screenshot", { - name: undefined + expect(Cypress.automation).to.be.calledWith("take:screenshot") + args = Cypress.automation.withArgs("take:screenshot").args[0][1] + expect(args).to.eql({ testId: runnable.id titles: [ "src/cy/commands/screenshot", "runnable:after:run:async", runnable.title ] + capture: "runner" + simple: true }) + describe "if screenshot has been taken in test", -> + beforeEach -> + cy.state("screenshotTaken", true) + + it "sends before/after events if screenshot has been taken in test", -> + Cypress.config("isInteractive", false) + @screenshotConfig.scale = false + cy.stub(Screenshot, "getConfig").returns(@screenshotConfig) + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + + cy.stub(Cypress, "action").log(false) + .callThrough() + .withArgs("cy:before:screenshot") + .yieldsAsync() + .withArgs("cy:after:screenshot") + .yieldsAsync() + + test = { id: "123", err: new Error() } + runnable = cy.state("runnable") + + Cypress.action("runner:runnable:after:run:async", test, runnable) + .then -> + expect(Cypress.action).to.be.calledWith("cy:before:screenshot", { + id: runnable.id + isOpen: true + appOnly: false + scale: true + waitForCommandSynchronization: true + disableTimersAndAnimations: true + blackout: [] + }) + expect(Cypress.action).to.be.calledWith("cy:after:screenshot", { + id: runnable.id + isOpen: false + appOnly: false + scale: true + waitForCommandSynchronization: true + disableTimersAndAnimations: true + blackout: [] + }) + + it "does not send simple: true", -> + Cypress.config("isInteractive", false) + cy.stub(Screenshot, "getConfig").returns(@screenshotConfig) + + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + + test = { + id: "123" + err: new Error + } + + runnable = cy.state("runnable") + + Cypress.action("runner:runnable:after:run:async", test, runnable) + .then -> + expect(Cypress.automation.withArgs("take:screenshot")).to.be.calledOnce + args = _.omit(Cypress.automation.withArgs("take:screenshot").args[0][1], "clip", "viewport", "userClip") + expect(args).to.eql({ + testId: runnable.id + titles: [ + "src/cy/commands/screenshot", + "runnable:after:run:async", + "if screenshot has been taken in test" + runnable.title + ] + capture: "runner" + }) + context "runnable:after:run:async hooks", -> beforeEach -> Cypress.config("isInteractive", false) + cy.stub(Screenshot, "getConfig").returns(@screenshotConfig) - Cypress.automation.withArgs("take:screenshot").resolves({}) - - cy.stub(Cypress, "action").log(false) - .callThrough() - .withArgs("cy:test:set:state") - .yieldsAsync() + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) test = { + id: "123" err: new Error } - runnable = cy.state("runnable") Cypress.action("runner:runnable:after:run:async", test, runnable) .then -> - ## this should mutate this property - expect(test.isOpen).to.be.true - - expect(Cypress.action).to.be.calledWith("cy:test:set:state", test) - expect(Cypress.automation).to.be.calledWith("take:screenshot", { - name: undefined + expect(Cypress.automation).to.be.calledWith("take:screenshot") + args = Cypress.automation.withArgs("take:screenshot").args[0][1] + expect(_.omit(args, "clip", "userClip", "viewport")).to.eql({ testId: runnable.id titles: [ "src/cy/commands/screenshot", @@ -117,73 +219,332 @@ describe "src/cy/commands/screenshot", -> "takes screenshot of hook title with test", '"before each" hook' ] + capture: "runner" + simple: true }) it "takes screenshot of hook title with test", -> context "#screenshot", -> - it "nulls out current subject", -> - Cypress.automation.withArgs("take:screenshot").resolves({path: "foo/bar.png", size: "100 kB"}) - - cy.screenshot().should("be.null") + beforeEach -> + cy.stub(Screenshot, "getConfig").returns(@screenshotConfig) it "sets name to undefined when not passed name", -> runnable = cy.state("runnable") runnable.title = "foo bar" - Cypress.automation.withArgs("take:screenshot").resolves({path: "foo/bar.png", size: "100 kB"}) + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) cy.screenshot().then -> - expect(Cypress.automation).to.be.calledWith("take:screenshot", { - name: undefined - testId: runnable.id - titles: [ - "src/cy/commands/screenshot", - "#screenshot", - "foo bar" - ] - }) + expect(Cypress.automation.withArgs("take:screenshot").args[0][1].name).to.be.undefined it "can pass name", -> runnable = cy.state("runnable") runnable.title = "foo bar" - Cypress.automation.withArgs("take:screenshot").resolves({path: "foo/bar.png", size: "100 kB"}) + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) cy.screenshot("my/file").then -> - expect(Cypress.automation).to.be.calledWith("take:screenshot", { - name: "my/file" - testId: runnable.id - titles: [ - "src/cy/commands/screenshot", - "#screenshot", - "foo bar" - ] - }) + expect(Cypress.automation.withArgs("take:screenshot").args[0][1].name).to.equal("my/file") - it "opens and closes the test", -> - runnable = cy.state("runnable") + it "calls beforeScreenshot callback with document", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.stub(Screenshot, "callBeforeScreenshot") + cy.spy(Cypress, "action").log(false) - Cypress.automation.withArgs("take:screenshot").resolves({}) + cy + .screenshot("foo") + .then -> + expect(Screenshot.callBeforeScreenshot).to.be.calledWith(cy.state("document")) - testSetState = cy.spy(Cypress, "action").log(false).withArgs("cy:test:set:state") + it "calls afterScreenshot callback with document", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.stub(Screenshot, "callAfterScreenshot") + cy.spy(Cypress, "action").log(false) cy .screenshot("foo") .then -> - expect(testSetState.firstCall).to.be.calledWith("cy:test:set:state", { - id: runnable.id - isOpen: true - }) + expect(Screenshot.callAfterScreenshot).to.be.calledWith(cy.state("document")) + + it "pauses then unpauses timers if disableTimersAndAnimations is true", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + cy.spy(cy, "pauseTimers") - expect(testSetState.secondCall).to.be.calledWith("cy:test:set:state", { - id: runnable.id - isOpen: false + cy + .screenshot("foo") + .then -> + expect(cy.pauseTimers).to.be.calledWith(true) + expect(cy.pauseTimers).to.be.calledWith(false) + + it "does not pause timers if disableTimersAndAnimations is false", -> + @screenshotConfig.disableTimersAndAnimations = false + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + + cy + .screenshot("foo") + .then -> + expect(Cypress.action.withArgs("cy:pause:timers")).not.to.be.called + + it "sends clip as userClip if specified", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + clip = { width: 100, height: 100, x: 0, y: 0 } + + cy + .screenshot({ clip }) + .then -> + expect(Cypress.automation.withArgs("take:screenshot").args[0][1].userClip).to.equal(clip) + + it "sends viewport dimensions of main browser window", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + + cy + .screenshot() + .then -> + expect(Cypress.automation.withArgs("take:screenshot").args[0][1].viewport).to.eql({ + width: $(window.parent).width() + height: $(window.parent).height() }) + it "yields an object with details", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + + expected = _.extend({}, @serverResult, @screenshotConfig, { + name: "name" + scaled: true + }) + expected = _.omit(expected, "blackout", "dimensions", "screenshotOnRunFailure", "scale") + + cy + .screenshot("name") + .then (result) => + actual = _.omit(result, "blackout", "dimensions", "duration") + expect(actual).to.eql(expected) + expect(result.blackout).to.eql(@screenshotConfig.blackout) + expect(result.dimensions).to.eql(@serverResult.dimensions) + expect(result.duration).to.be.a("number") + expect(result.duration).to.be.gt(0) + + describe "before/after events", -> + beforeEach -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + + it "sends before:screenshot", -> + runnable = cy.state("runnable") + cy + .screenshot("foo") + .then -> + expect(Cypress.action.withArgs("cy:before:screenshot")).to.be.calledOnce + expect(Cypress.action.withArgs("cy:before:screenshot").args[0][1]).to.eql({ + id: runnable.id + isOpen: true + appOnly: true + scale: true + waitForCommandSynchronization: false + disableTimersAndAnimations: true + blackout: [".foo"] + }) + + it "sends after:screenshot", -> + runnable = cy.state("runnable") + cy + .screenshot("foo") + .then -> + expect(Cypress.action.withArgs("cy:after:screenshot")).to.be.calledOnce + expect(Cypress.action.withArgs("cy:after:screenshot").args[0][1]).to.eql({ + id: runnable.id + isOpen: false + appOnly: true + scale: true + waitForCommandSynchronization: false + disableTimersAndAnimations: true + blackout: [".foo"] + }) + + it "always sends scale: true, waitForCommandSynchronization: true, and blackout: [] for non-app captures", -> + runnable = cy.state("runnable") + @screenshotConfig.capture = "runner" + @screenshotConfig.scale = false + + cy + .screenshot("foo") + .then -> + expect(Cypress.action.withArgs("cy:before:screenshot").args[0][1]).to.eql({ + id: runnable.id + isOpen: true + appOnly: false + scale: true + waitForCommandSynchronization: true + disableTimersAndAnimations: true + blackout: [] + }) + + it "always sends waitForCommandSynchronization: false for viewport/fullPage captures", -> + runnable = cy.state("runnable") + @screenshotConfig.waitForAnimations = true + + cy + .screenshot("foo") + .then -> + expect(Cypress.action.withArgs("cy:before:screenshot").args[0][1]).to.eql({ + id: runnable.id + isOpen: true + appOnly: true + scale: true + waitForCommandSynchronization: false + disableTimersAndAnimations: true + blackout: [".foo"] + }) + + describe "capture: fullPage", -> + beforeEach -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + cy.viewport(600, 200) + cy.visit("/fixtures/screenshots.html") + + it "takes a screenshot for each time it needs to scroll", -> + cy.screenshot({ capture: "fullPage" }) + .then -> + expect(Cypress.automation.withArgs("take:screenshot")).to.be.calledThrice + + it "sends capture: fullPage", -> + cy.screenshot({ capture: "fullPage" }) + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].capture).to.equal("fullPage") + expect(take.args[1][1].capture).to.equal("fullPage") + expect(take.args[2][1].capture).to.equal("fullPage") + + it "sends number of current screenshot for each time it needs to scroll", -> + cy.screenshot({ capture: "fullPage" }) + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].current).to.equal(1) + expect(take.args[1][1].current).to.equal(2) + expect(take.args[2][1].current).to.equal(3) + + it "sends total number of screenshots for each time it needs to scroll", -> + cy.screenshot({ capture: "fullPage" }) + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].total).to.equal(3) + expect(take.args[1][1].total).to.equal(3) + expect(take.args[2][1].total).to.equal(3) + + it "scrolls the window to the right place for each screenshot", -> + win = cy.state("window") + win.scrollTo(0, 100) + scrollTo = cy.spy(win, "scrollTo") + cy.screenshot({ capture: "fullPage" }) + .then -> + expect(scrollTo.getCall(0).args.join(",")).to.equal("0,0") + expect(scrollTo.getCall(1).args.join(",")).to.equal("0,200") + expect(scrollTo.getCall(2).args.join(",")).to.equal("0,400") + + it "scrolls the window back to the original place", -> + win = cy.state("window") + win.scrollTo(0, 100) + scrollTo = cy.spy(win, "scrollTo") + cy.screenshot({ capture: "fullPage" }) + .then -> + expect(scrollTo.getCall(3).args.join(",")).to.equal("0,100") + + it "sends the right clip values", -> + cy.screenshot({ capture: "fullPage" }) + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].clip).to.eql({ x: 0, y: 0, width: 600, height: 200 }) + expect(take.args[1][1].clip).to.eql({ x: 0, y: 0, width: 600, height: 200 }) + expect(take.args[2][1].clip).to.eql({ x: 0, y: 120, width: 600, height: 80 }) + + describe "element capture", -> + beforeEach -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + cy.spy(Cypress, "action").log(false) + cy.viewport(600, 200) + cy.visit("/fixtures/screenshots.html") + + it "takes a screenshot for each time it needs to scroll", -> + cy.get(".tall-element").screenshot() + .then -> + expect(Cypress.automation.withArgs("take:screenshot")).to.be.calledTwice + + it "sends number of current screenshot for each time it needs to scroll", -> + cy.get(".tall-element").screenshot() + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].current).to.equal(1) + expect(take.args[1][1].current).to.equal(2) + + it "sends total number of screenshots for each time it needs to scroll", -> + cy.get(".tall-element").screenshot() + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].total).to.equal(2) + expect(take.args[1][1].total).to.equal(2) + + it "scrolls the window to the right place for each screenshot", -> + win = cy.state("window") + win.scrollTo(0, 100) + scrollTo = cy.spy(win, "scrollTo") + cy.get(".tall-element").screenshot() + .then -> + expect(scrollTo.getCall(0).args.join(",")).to.equal("0,140") + expect(scrollTo.getCall(1).args.join(",")).to.equal("0,340") + + it "scrolls the window back to the original place", -> + win = cy.state("window") + win.scrollTo(0, 100) + scrollTo = cy.spy(win, "scrollTo") + cy.get(".tall-element").screenshot() + .then -> + expect(scrollTo.getCall(2).args.join(",")).to.equal("0,100") + + it "sends the right clip values for elements that need scrolling", -> + cy.get(".tall-element").screenshot() + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].clip).to.eql({ x: 20, y: 0, width: 560, height: 200 }) + expect(take.args[1][1].clip).to.eql({ x: 20, y: 60, width: 560, height: 120 }) + + it "sends the right clip values for elements that don't need scrolling", -> + cy.get(".short-element").screenshot() + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].clip).to.eql({ x: 40, y: 0, width: 200, height: 100 }) + + it "works with cy.within()", -> + cy.get(".short-element").within -> + cy.screenshot() + .then -> + take = Cypress.automation.withArgs("take:screenshot") + expect(take.args[0][1].clip).to.eql({ x: 40, y: 0, width: 200, height: 100 }) + + it "coerces capture option into 'app'", -> + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + + cy.get(".short-element").screenshot({ capture: "runner" }) + .then -> + expect(Cypress.action.withArgs("cy:before:screenshot").args[0][1].appOnly).to.be.true + expect(Cypress.automation.withArgs("take:screenshot").args[0][1].capture).to.equal("viewport") + + it "yields an object with el set to subject", -> + cy.get(".short-element").then ($el) -> + cy + .get(".short-element") + .screenshot() + .then ({ el }) => + expect(el[0]).to.equal($el[0]) + describe "timeout", -> beforeEach -> - Cypress.automation.withArgs("take:screenshot").resolves({path: "foo/bar.png", size: "100 kB"}) + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) it "sets timeout to Cypress.config(responseTimeout)", -> Cypress.config("responseTimeout", 2500) @@ -227,8 +588,58 @@ describe "src/cy/commands/screenshot", -> @lastLog = log @logs.push(log) + @assertErrorMessage = (message, done) => + cy.on "fail", (err) => + expect(@lastLog.get("error").message).to.eq(message) + done() + return null + it "throws if capture is not a string", (done) -> + @assertErrorMessage("cy.screenshot() 'capture' option must be one of the following: 'fullPage', 'viewport', or 'runner'. You passed: true", done) + cy.screenshot({ capture: true }) + + it "throws if capture is not a valid option", (done) -> + @assertErrorMessage("cy.screenshot() 'capture' option must be one of the following: 'fullPage', 'viewport', or 'runner'. You passed: foo", done) + cy.screenshot({ capture: "foo" }) + + it "throws if scale is not a boolean", (done) -> + @assertErrorMessage("cy.screenshot() 'scale' option must be a boolean. You passed: foo", done) + cy.screenshot({ scale: "foo" }) + + it "throws if disableTimersAndAnimations is not a boolean", (done) -> + @assertErrorMessage("cy.screenshot() 'disableTimersAndAnimations' option must be a boolean. You passed: foo", done) + cy.screenshot({ disableTimersAndAnimations: "foo" }) + + it "throws if blackout is not an array", (done) -> + @assertErrorMessage("cy.screenshot() 'blackout' option must be an array of strings. You passed: foo", done) + cy.screenshot({ blackout: "foo" }) + + it "throws if blackout is not an array of strings", (done) -> + @assertErrorMessage("cy.screenshot() 'blackout' option must be an array of strings. You passed: true", done) + cy.screenshot({ blackout: [true] }) + + it "throws if clip is not an object", (done) -> + @assertErrorMessage("cy.screenshot() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: true", done) + cy.screenshot({ clip: true }) + + it "throws if clip is lacking proper keys", (done) -> + @assertErrorMessage("cy.screenshot() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: {x: 5}", done) + cy.screenshot({ clip: { x: 5 } }) + + it "throws if clip has extraneous keys", (done) -> + @assertErrorMessage("cy.screenshot() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: Object{5}", done) + cy.screenshot({ clip: { width: 100, height: 100, x: 5, y: 5, foo: 10 } }) + + it "throws if clip has non-number values", (done) -> + @assertErrorMessage("cy.screenshot() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: Object{4}", done) + cy.screenshot({ clip: { width: 100, height: 100, x: 5, y: "5" } }) + + it "throws if element capture with multiple elements", (done) -> + @assertErrorMessage("cy.screenshot() only works for a single element. You attempted to screenshot 4 elements.", done) + cy.visit("/fixtures/screenshots.html") + cy.get(".multiple").screenshot() + it "logs once on error", (done) -> error = new Error("some error") error.name = "foo" @@ -266,7 +677,7 @@ describe "src/cy/commands/screenshot", -> describe ".log", -> beforeEach -> - Cypress.automation.withArgs("take:screenshot").resolves({path: "foo/bar.png", size: "100 kB"}) + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) cy.on "log:added", (attrs, log) => if attrs.name is "screenshot" @@ -293,7 +704,19 @@ describe "src/cy/commands/screenshot", -> expect(lastLog.get("snapshots")[0]).to.be.an("object") it "#consoleProps", -> - cy.screenshot().then -> - c = @lastLog.invoke("consoleProps") - expect(c["Saved"]).to.deep.eq "foo/bar.png" - expect(c["Size"]).to.eq "100 kB" + Cypress.automation.withArgs("take:screenshot").resolves(@serverResult) + + expected = _.extend({}, @serverResult, @screenshotConfig, { + Command: "screenshot" + scaled: true + }) + expected = _.omit(expected, "blackout", "dimensions", "screenshotOnRunFailure", "scale") + + cy.screenshot().then => + consoleProps = @lastLog.invoke("consoleProps") + actual = _.omit(consoleProps, "blackout", "dimensions", "duration") + { width, height } = @serverResult.dimensions + expect(actual).to.eql(expected) + expect(consoleProps.blackout).to.eql(@screenshotConfig.blackout) + expect(consoleProps.dimensions).to.eql("#{width}px x #{height}px") + expect(consoleProps.duration).to.match(/^\d+ms$/) diff --git a/packages/driver/test/cypress/integration/commands/task_spec.coffee b/packages/driver/test/cypress/integration/commands/task_spec.coffee new file mode 100644 index 000000000000..c597f5e1c831 --- /dev/null +++ b/packages/driver/test/cypress/integration/commands/task_spec.coffee @@ -0,0 +1,213 @@ +_ = Cypress._ +Promise = Cypress.Promise + +describe "src/cy/commands/task", -> + context "#task", -> + beforeEach -> + Cypress.config("taskTimeout", 2500) + + cy.stub(Cypress, "backend").callThrough() + + it "calls Cypress.backend('task') with the right options", -> + Cypress.backend.resolves(null) + + cy.task("foo").then -> + expect(Cypress.backend).to.be.calledWith("task", { + task: "foo" + timeout: 2500 + arg: undefined + }) + + it "passes through arg", -> + Cypress.backend.resolves(null) + + cy.task("foo", { foo: "foo" }).then -> + expect(Cypress.backend).to.be.calledWith("task", { + task: "foo" + timeout: 2500 + arg: { + foo: "foo" + } + }) + + it "really works", -> + cy.task("return:arg", "works").should("eq", "works") + + describe ".log", -> + beforeEach -> + @logs = [] + + cy.on "log:added", (attrs, log) => + @lastLog = log + @logs.push(log) + + Cypress.backend.resolves(null) + + return null + + it "can turn off logging", -> + cy.task("foo", null, { log: false }).then -> + logs = _.filter @logs, (log) -> + log.get("name") is "task" + + expect(logs.length).to.eq(0) + + it "logs immediately before resolving", -> + cy.on "log:added", (attrs, log) => + if attrs.name is "task" + expect(log.get("state")).to.eq("pending") + expect(log.get("message")).to.eq("foo") + + cy.task("foo").then => + throw new Error("failed to log before resolving") unless @lastLog + + describe "timeout", -> + beforeEach -> + Cypress.backend.resolves(null) + + it "defaults timeout to Cypress.config(taskTimeout)", -> + timeout = cy.spy(Promise.prototype, "timeout") + + cy.task("foo").then -> + expect(timeout).to.be.calledWith(2500) + + it "can override timeout", -> + timeout = cy.spy(Promise.prototype, "timeout") + + cy.task("foo", null, { timeout: 1000 }).then -> + expect(timeout).to.be.calledWith(1000) + + it "clears the current timeout and restores after success", -> + cy.timeout(100) + + clearTimeout = cy.spy(cy, "clearTimeout") + + cy.on "task", => + expect(clearTimeout).to.be.calledOnce + + cy.task("foo").then -> + expect(cy.timeout()).to.eq(100) + + describe "errors", -> + beforeEach -> + Cypress.config("defaultCommandTimeout", 50) + + @logs = [] + + cy.on "log:added", (attrs, log) => + if attrs.name is "task" + @lastLog = log + @logs.push(log) + + return null + + it "throws when task is absent", (done) -> + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + expect(err.message).to.eq("cy.task() must be passed a non-empty string as its 1st argument. You passed: ''.") + done() + + cy.task() + + it "throws when task isn't a string", (done) -> + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + expect(err.message).to.eq("cy.task() must be passed a non-empty string as its 1st argument. You passed: '3'.") + done() + + cy.task(3) + + it "throws when task is an empty string", (done) -> + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + expect(err.message).to.eq("cy.task() must be passed a non-empty string as its 1st argument. You passed: ''.") + done() + + cy.task('') + + it "throws when the task errors", (done) -> + Cypress.backend.rejects(new Error("task failed")) + + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + + expect(err.message).to.include("cy.task('foo') failed with the following error:") + expect(err.message).to.include("Error: task failed") + done() + + cy.task("foo") + + it "throws when task is not registered by plugin", (done) -> + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + + expect(err.message).to.eq("cy.task('bar') failed with the following error:\n\nThe task 'bar' was not handled in the plugins file. The following tasks are registered: return:arg, wait\n\nFix this in your plugins file here:\n#{Cypress.config('pluginsFile')}\n\nhttps://on.cypress.io/api/task") + done() + + cy.task("bar") + + it "throws after timing out", (done) -> + Cypress.backend.resolves(Promise.delay(250)) + + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + expect(err.message).to.eq("cy.task('foo') timed out after waiting 50ms.") + done() + + cy.task("foo", null, { timeout: 50 }) + + it "logs once on error", (done) -> + Cypress.backend.rejects(new Error("task failed")) + + cy.on "fail", (err) => + lastLog = @lastLog + + expect(@logs.length).to.eq(1) + expect(lastLog.get("error")).to.eq(err) + expect(lastLog.get("state")).to.eq("failed") + done() + + cy.task("foo") + + it "can timeout from the backend's response", (done) -> + err = new Error("timeout") + err.timedOut = true + + Cypress.backend.rejects(err) + + cy.on "fail", (err) -> + expect(err.message).to.include("cy.task('wait') timed out after waiting 100ms.") + done() + + cy.task("wait", null, { timeout: 100 }) + + it "can really time out", (done) -> + cy.on "fail", (err) -> + expect(err.message).to.include("cy.task('wait') timed out after waiting 100ms.") + done() + + cy.task("wait", null, { timeout: 100 }) diff --git a/packages/driver/test/cypress/integration/commands/window_spec.coffee b/packages/driver/test/cypress/integration/commands/window_spec.coffee index 6688a94665b4..b202de819f7c 100644 --- a/packages/driver/test/cypress/integration/commands/window_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/window_spec.coffee @@ -612,26 +612,26 @@ describe "src/cy/commands/window", -> it "throws when passed negative numbers", (done) -> cy.on "fail", (err) => expect(@logs.length).to.eq(1) - expect(err.message).to.eq "cy.viewport() width and height must be between 200px and 3000px." + expect(err.message).to.eq "cy.viewport() width and height must be between 20px and 3000px." done() cy.viewport(800, -600) - it "throws when passed width less than 200", (done) -> + it "throws when passed width less than 20", (done) -> cy.on "fail", (err) => expect(@logs.length).to.eq(1) - expect(err.message).to.eq "cy.viewport() width and height must be between 200px and 3000px." + expect(err.message).to.eq "cy.viewport() width and height must be between 20px and 3000px." done() - cy.viewport(199, 600) + cy.viewport(19, 600) - it "does not throw when passed width equal to 200", -> - cy.viewport(200, 600) + it "does not throw when passed width equal to 20", -> + cy.viewport(20, 600) it "throws when passed height greater than than 3000", (done) -> cy.on "fail", (err) => expect(@logs.length).to.eq(1) - expect(err.message).to.eq "cy.viewport() width and height must be between 200px and 3000px." + expect(err.message).to.eq "cy.viewport() width and height must be between 20px and 3000px." done() cy.viewport(1000, 3001) diff --git a/packages/driver/test/cypress/integration/cy/snapshot_spec.coffee b/packages/driver/test/cypress/integration/cy/snapshot_spec.coffee index 86d649b1f043..89a490fe1fec 100644 --- a/packages/driver/test/cypress/integration/cy/snapshot_spec.coffee +++ b/packages/driver/test/cypress/integration/cy/snapshot_spec.coffee @@ -85,6 +85,54 @@ describe "src/cy/snapshot", -> .load(onLoad) .appendTo(cy.$$("head")) + it "provides media-less stylesheets", (done) -> + onLoad = -> + ## need to for appended stylesheet to load + { headStyles } = cy.createSnapshot(@$el) + + expect(headStyles[0]).to.include(".foo { color: green; }") + done() + + $("") + .load(onLoad) + .appendTo(cy.$$("head")) + + it "provides media=screen stylesheets", (done) -> + onLoad = -> + ## need to for appended stylesheet to load + { headStyles } = cy.createSnapshot(@$el) + + expect(headStyles[0]).to.include(".foo { color: green; }") + done() + + $("") + .load(onLoad) + .appendTo(cy.$$("head")) + + it "provides media=all stylesheets", (done) -> + onLoad = -> + ## need to for appended stylesheet to load + { headStyles } = cy.createSnapshot(@$el) + + expect(headStyles[0]).to.include(".foo { color: green; }") + done() + + $("") + .load(onLoad) + .appendTo(cy.$$("head")) + + it "does not provide non-screen stylesheets", (done) -> + onLoad = -> + ## need to for appended stylesheet to load + { headStyles } = cy.createSnapshot(@$el) + + expect(headStyles).to.have.length(0) + done() + + $("") + .load(onLoad) + .appendTo(cy.$$("head")) + it "provides object with href of external stylesheets in head", (done) -> onLoad = -> ## need to for appended stylesheet to load diff --git a/packages/driver/test/cypress/integration/cypress/screenshot_spec.coffee b/packages/driver/test/cypress/integration/cypress/screenshot_spec.coffee new file mode 100644 index 000000000000..45ab8e36b78e --- /dev/null +++ b/packages/driver/test/cypress/integration/cypress/screenshot_spec.coffee @@ -0,0 +1,145 @@ +{ Screenshot, $ } = Cypress + +DEFAULTS = { + capture: "fullPage" + scale: false + disableTimersAndAnimations: true + screenshotOnRunFailure: true + blackout: [] +} + +describe "src/cypress/screenshot", -> + beforeEach -> + ## reset state since this is a singleton + Screenshot.reset() + + it "has defaults", -> + expect(Screenshot.getConfig()).to.deep.eq(DEFAULTS) + expect(-> Screenshot.callBeforeScreenshot()).not.to.throw() + expect(-> Screenshot.callAfterScreenshot()).not.to.throw() + + context ".getConfig", -> + it "returns copy of config", -> + config = Screenshot.getConfig() + config.blackout.push(".foo") + expect(Screenshot.getConfig().blackout).to.deep.eq(DEFAULTS.blackout) + + context ".defaults", -> + it "is noop if not called with any valid properties", -> + Screenshot.defaults({}) + expect(Screenshot.getConfig()).to.deep.eq(DEFAULTS) + expect(-> Screenshot.callBeforeScreenshot()).not.to.throw() + expect(-> Screenshot.callAfterScreenshot()).not.to.throw() + + it "sets capture if specified", -> + Screenshot.defaults({ + capture: "runner" + }) + expect(Screenshot.getConfig().capture).to.eql("runner") + + it "sets scale if specified", -> + Screenshot.defaults({ + scale: true + }) + expect(Screenshot.getConfig().scale).to.equal(true) + + it "sets disableTimersAndAnimations if specified", -> + Screenshot.defaults({ + disableTimersAndAnimations: false + }) + expect(Screenshot.getConfig().disableTimersAndAnimations).to.equal(false) + + it "sets screenshotOnRunFailure if specified", -> + Screenshot.defaults({ + screenshotOnRunFailure: false + }) + expect(Screenshot.getConfig().screenshotOnRunFailure).to.equal(false) + + it "sets clip if specified", -> + Screenshot.defaults({ + clip: { width: 200, height: 100, x: 0, y: 0 } + }) + expect(Screenshot.getConfig().clip).to.eql({ width: 200, height: 100, x: 0, y:0 }) + + it "sets beforeScreenshot if specified", -> + beforeScreenshot = cy.stub() + Screenshot.defaults({ beforeScreenshot }) + Screenshot.callBeforeScreenshot() + expect(beforeScreenshot).to.be.called + + it "sets afterScreenshot if specified", -> + afterScreenshot = cy.stub() + Screenshot.defaults({ afterScreenshot }) + Screenshot.callAfterScreenshot() + expect(afterScreenshot).to.be.called + + describe "errors", -> + it "throws if not passed an object", -> + expect => + Screenshot.defaults() + .to.throw("Cypress.Screenshot.defaults() must be called with an object. You passed: ") + + it "throws if capture is not a string", -> + expect => + Screenshot.defaults({ capture: true }) + .to.throw("Cypress.Screenshot.defaults() 'capture' option must be one of the following: 'fullPage', 'viewport', or 'runner'. You passed: true") + + it "throws if capture is not a valid option", -> + expect => + Screenshot.defaults({ capture: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'capture' option must be one of the following: 'fullPage', 'viewport', or 'runner'. You passed: foo") + + it "throws if scale is not a boolean", -> + expect => + Screenshot.defaults({ scale: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'scale' option must be a boolean. You passed: foo") + + it "throws if disableTimersAndAnimations is not a boolean", -> + expect => + Screenshot.defaults({ disableTimersAndAnimations: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'disableTimersAndAnimations' option must be a boolean. You passed: foo") + + it "throws if screenshotOnRunFailure is not a boolean", -> + expect => + Screenshot.defaults({ screenshotOnRunFailure: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'screenshotOnRunFailure' option must be a boolean. You passed: foo") + + it "throws if blackout is not an array", -> + expect => + Screenshot.defaults({ blackout: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'blackout' option must be an array of strings. You passed: foo") + + it "throws if blackout is not an array of strings", -> + expect => + Screenshot.defaults({ blackout: [true] }) + .to.throw("Cypress.Screenshot.defaults() 'blackout' option must be an array of strings. You passed: true") + + it "throws if clip is not an object", -> + expect => + Screenshot.defaults({ clip: true }) + .to.throw("Cypress.Screenshot.defaults() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: true") + + it "throws if clip is lacking proper keys", -> + expect => + Screenshot.defaults({ clip: { x: 5 } }) + .to.throw("Cypress.Screenshot.defaults() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: {x: 5}") + + it "throws if clip has extraneous keys", -> + expect => + Screenshot.defaults({ clip: { width: 100, height: 100, x: 5, y: 5, foo: 10 } }) + .to.throw("Cypress.Screenshot.defaults() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: Object{5}") + + it "throws if clip has non-number values", -> + expect => + Screenshot.defaults({ clip: { width: 100, height: 100, x: 5, y: "5" } }) + .to.throw("Cypress.Screenshot.defaults() 'clip' option must be an object of with the keys { width, height, x, y } and number values. You passed: Object{4}") + + it "throws if beforeScreenshot is not a function", -> + expect => + Screenshot.defaults({ beforeScreenshot: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'beforeScreenshot' option must be a function. You passed: foo") + + it "throws if afterScreenshot is not a function", -> + expect => + Screenshot.defaults({ afterScreenshot: "foo" }) + .to.throw("Cypress.Screenshot.defaults() 'afterScreenshot' option must be a function. You passed: foo") diff --git a/packages/driver/test/cypress/integration/cypress/utils_spec.coffee b/packages/driver/test/cypress/integration/cypress/utils_spec.coffee index 7ed4e4a1f4f9..030ab01f4647 100644 --- a/packages/driver/test/cypress/integration/cypress/utils_spec.coffee +++ b/packages/driver/test/cypress/integration/cypress/utils_spec.coffee @@ -1,5 +1,6 @@ _ = Cypress._ $utils = Cypress.utils +Promise = Cypress.Promise describe "driver/src/cypress/utils", -> context ".cloneErr", -> diff --git a/packages/driver/test/cypress/integration/issues/1436_spec.js b/packages/driver/test/cypress/integration/issues/1436_spec.js new file mode 100644 index 000000000000..b8f6bf1a36ef --- /dev/null +++ b/packages/driver/test/cypress/integration/issues/1436_spec.js @@ -0,0 +1,21 @@ +// https://github.com/cypress-io/cypress/issues/1436 +describe('issue 1436', () => { + it('returns the AUT window, not Cypress top', () => { + cy.visit('/fixtures/issue-1436.html') + cy.window().then((win) => { + win.__app__ = true + + expect(win.getParent(win).__app__).to.be.true + expect(win.getParentMin(win).__app__).to.be.true + }) + }) + + it('can visit jira', () => { + // no javascript errors should have been thrown. + // NOTE: this is potentially a bad idea because we don't + // control Jira, and therefore they could push changes + // which cause Cypress to throw (or be down). if this + // ends up happening we'll need to remove this test. + cy.visit('https://jira.atlassian.com/secure/BrowseProjects.jspa?selectedCategory=all&selectedProjectType=all') + }) +}) diff --git a/packages/driver/test/cypress/plugins/index.js b/packages/driver/test/cypress/plugins/index.js new file mode 100644 index 000000000000..40bcc91b2826 --- /dev/null +++ b/packages/driver/test/cypress/plugins/index.js @@ -0,0 +1,12 @@ +const Promise = require('bluebird') + +module.exports = (on) => { + on('task', { + 'return:arg' (arg) { + return arg + }, + 'wait' () { + return Promise.delay(2000) + }, + }) +} diff --git a/packages/driver/test/unit_old/cypress/runner_spec.coffee b/packages/driver/test/unit_old/cypress/runner_spec.coffee index e7430f720671..3ed987f861dc 100644 --- a/packages/driver/test/unit_old/cypress/runner_spec.coffee +++ b/packages/driver/test/unit_old/cypress/runner_spec.coffee @@ -321,8 +321,8 @@ describe "$Cypress.Runner API", -> it "sets test hook to hook", -> expect(@relatedTest.hookName).to.eq "before each" - it "sets test failedFromHook", -> - expect(@relatedTest.failedFromHook).to.be.true + it "sets test failedFromHookId", -> + expect(@relatedTest.failedFromHookId).to.be.true context "#getTestFromHook", -> beforeEach -> diff --git a/packages/example/README.md b/packages/example/README.md index 788ce57e7ffc..ee259c4529ae 100644 --- a/packages/example/README.md +++ b/packages/example/README.md @@ -1,4 +1,4 @@ -## Example +## Example This repo contains the source code for pushing out [https://example.cypress.io](https://example.cypress.io). @@ -6,7 +6,7 @@ The actual example repo you're probably looking for is [the kitchen sink app her **THERE'S LIKELY NO REASON YOU NEED TO EDIT ANY OF THE CODE ON THIS REPO.** -- Want to edit the `example_spec.js` file? -> edit it [here](https://github.com/cypress-io/cypress-example-kitchensink/blob/master/cypress/integration/example_spec.js) instead. +- Want to edit the `example` tests? -> edit it [here](https://github.com/cypress-io/cypress-example-kitchensink/blob/master/cypress/integration/examples) instead. - Want to edit the actual [https://example.cypress.io](https://example.cypress.io) website? edit it [here](https://github.com/cypress-io/cypress-example-kitchensink/tree/master/app) instead. ## Developing @@ -23,7 +23,7 @@ After running `npm install` you must build the app + spec files. npm run build ``` -This copies the src files from [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink), modifies them to point to `https://example.cypress.io` and creates the `example_spec.js`. +This copies the src files from [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink), modifies them to point to `https://example.cypress.io` and creates the `example` tests. ## Deploying diff --git a/packages/example/bin/convert.js b/packages/example/bin/convert.js index 5c586f3fa5f7..9e548350a03c 100755 --- a/packages/example/bin/convert.js +++ b/packages/example/bin/convert.js @@ -33,14 +33,14 @@ function replaceStringsIn (file) { }) } -glob('./app/**/*.html', { realpath: true }, function (err, files) { +glob('./app/**/*.html', { realpath: true }, (err, htmlFiles) => { if (err) throw err - const spec = path.join(process.cwd(), 'cypress', 'integration', 'example_spec.js') - - files.push(spec) + glob('./cypress/integration/examples/**/*', { realpath: true }, (err, specFiles) => { + if (err) throw err - files.forEach(function (file) { - return replaceStringsIn(file) + htmlFiles.concat(specFiles).forEach(function (file) { + return replaceStringsIn(file) + }) }) }) diff --git a/packages/example/cypress/integration/example_spec.js b/packages/example/cypress/integration/example_spec.js deleted file mode 100644 index 562d88deca03..000000000000 --- a/packages/example/cypress/integration/example_spec.js +++ /dev/null @@ -1,1497 +0,0 @@ -// -// **** Kitchen Sink Tests **** -// -// This app was developed to demonstrate -// how to write tests in Cypress utilizing -// all of the available commands -// -// Feel free to modify this spec in your -// own application as a jumping off point - -// Please read our "Introduction to Cypress" -// https://on.cypress.io/introduction-to-cypress - -describe('Kitchen Sink', function () { - it('.should() - assert that is correct', function () { - // https://on.cypress.io/visit - cy.visit('https://example.cypress.io') - - // Here we've made our first assertion using a '.should()' command. - // An assertion is comprised of a chainer, subject, and optional value. - - // https://on.cypress.io/should - // https://on.cypress.io/and - - // https://on.cypress.io/title - cy.title().should('include', 'Kitchen Sink') - // ↲ ↲ ↲ - // subject chainer value - }) - - context('Querying', function () { - beforeEach(function () { - // Visiting our app before each test removes any state build up from - // previous tests. Visiting acts as if we closed a tab and opened a fresh one - cy.visit('https://example.cypress.io/commands/querying') - }) - - // Let's query for some DOM elements and make assertions - // The most commonly used query is 'cy.get()', you can - // think of this like the '$' in jQuery - - it('cy.get() - query DOM elements', function () { - // https://on.cypress.io/get - - // Get DOM elements by id - cy.get('#query-btn').should('contain', 'Button') - - // Get DOM elements by class - cy.get('.query-btn').should('contain', 'Button') - - cy.get('#querying .well>button:first').should('contain', 'Button') - // ↲ - // Use CSS selectors just like jQuery - }) - - it('cy.contains() - query DOM elements with matching content', function () { - // https://on.cypress.io/contains - cy.get('.query-list') - .contains('bananas').should('have.class', 'third') - - // we can pass a regexp to `.contains()` - cy.get('.query-list') - .contains(/^b\w+/).should('have.class', 'third') - - cy.get('.query-list') - .contains('apples').should('have.class', 'first') - - // passing a selector to contains will yield the selector containing the text - cy.get('#querying') - .contains('ul', 'oranges').should('have.class', 'query-list') - - // `.contains()` will favor input[type='submit'], - // button, a, and label over deeper elements inside them - // this will not yield the <span> inside the button, - // but the <button> itself - cy.get('.query-button') - .contains('Save Form').should('have.class', 'btn') - }) - - it('.within() - query DOM elements within a specific element', function () { - // https://on.cypress.io/within - cy.get('.query-form').within(function () { - cy.get('input:first').should('have.attr', 'placeholder', 'Email') - cy.get('input:last').should('have.attr', 'placeholder', 'Password') - }) - }) - - it('cy.root() - query the root DOM element', function () { - // https://on.cypress.io/root - // By default, root is the document - cy.root().should('match', 'html') - - cy.get('.query-ul').within(function () { - // In this within, the root is now the ul DOM element - cy.root().should('have.class', 'query-ul') - }) - }) - }) - - context('Traversal', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/traversal') - }) - - // Let's query for some DOM elements and make assertions - - it('.children() - get child DOM elements', function () { - // https://on.cypress.io/children - cy.get('.traversal-breadcrumb').children('.active') - .should('contain', 'Data') - }) - - it('.closest() - get closest ancestor DOM element', function () { - // https://on.cypress.io/closest - cy.get('.traversal-badge').closest('ul') - .should('have.class', 'list-group') - }) - - it('.eq() - get a DOM element at a specific index', function () { - // https://on.cypress.io/eq - cy.get('.traversal-list>li').eq(1).should('contain', 'siamese') - }) - - it('.filter() - get DOM elements that match the selector', function () { - // https://on.cypress.io/filter - cy.get('.traversal-nav>li').filter('.active').should('contain', 'About') - }) - - it('.find() - get descendant DOM elements of the selector', function () { - // https://on.cypress.io/find - cy.get('.traversal-pagination').find('li').find('a') - .should('have.length', 7) - }) - - it('.first() - get first DOM element', function () { - // https://on.cypress.io/first - cy.get('.traversal-table td').first().should('contain', '1') - }) - - it('.last() - get last DOM element', function () { - // https://on.cypress.io/last - cy.get('.traversal-buttons .btn').last().should('contain', 'Submit') - }) - - it('.next() - get next sibling DOM element', function () { - // https://on.cypress.io/next - cy.get('.traversal-ul').contains('apples').next().should('contain', 'oranges') - }) - - it('.nextAll() - get all next sibling DOM elements', function () { - // https://on.cypress.io/nextall - cy.get('.traversal-next-all').contains('oranges') - .nextAll().should('have.length', 3) - }) - - it('.nextUntil() - get next sibling DOM elements until next el', function () { - // https://on.cypress.io/nextuntil - cy.get('#veggies').nextUntil('#nuts').should('have.length', 3) - }) - - it('.not() - remove DOM elements from set of DOM elements', function () { - // https://on.cypress.io/not - cy.get('.traversal-disabled .btn').not('[disabled]').should('not.contain', 'Disabled') - }) - - it('.parent() - get parent DOM element from DOM elements', function () { - // https://on.cypress.io/parent - cy.get('.traversal-mark').parent().should('contain', 'Morbi leo risus') - }) - - it('.parents() - get parent DOM elements from DOM elements', function () { - // https://on.cypress.io/parents - cy.get('.traversal-cite').parents().should('match', 'blockquote') - }) - - it('.parentsUntil() - get parent DOM elements from DOM elements until el', function () { - // https://on.cypress.io/parentsuntil - cy.get('.clothes-nav').find('.active').parentsUntil('.clothes-nav') - .should('have.length', 2) - }) - - it('.prev() - get previous sibling DOM element', function () { - // https://on.cypress.io/prev - cy.get('.birds').find('.active').prev().should('contain', 'Lorikeets') - }) - - it('.prevAll() - get all previous sibling DOM elements', function () { - // https://on.cypress.io/prevAll - cy.get('.fruits-list').find('.third').prevAll().should('have.length', 2) - }) - - it('.prevUntil() - get all previous sibling DOM elements until el', function () { - // https://on.cypress.io/prevUntil - cy.get('.foods-list').find('#nuts').prevUntil('#veggies') - }) - - it('.siblings() - get all sibling DOM elements', function () { - // https://on.cypress.io/siblings - cy.get('.traversal-pills .active').siblings().should('have.length', 2) - }) - }) - - context('Actions', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/actions') - }) - - // Let's perform some actions on DOM elements - // https://on.cypress.io/interacting-with-elements - - it('.type() - type into a DOM element', function () { - // https://on.cypress.io/type - cy.get('.action-email') - .type('fake@email.com').should('have.value', 'fake@email.com') - - // .type() with special character sequences - .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') - .type('{del}{selectall}{backspace}') - - // .type() with key modifiers - .type('{alt}{option}') //these are equivalent - .type('{ctrl}{control}') //these are equivalent - .type('{meta}{command}{cmd}') //these are equivalent - .type('{shift}') - - // Delay each keypress by 0.1 sec - .type('slow.typing@email.com', { delay: 100 }) - .should('have.value', 'slow.typing@email.com') - - cy.get('.action-disabled') - // Ignore error checking prior to type - // like whether the input is visible or disabled - .type('disabled error checking', { force: true }) - .should('have.value', 'disabled error checking') - }) - - it('.focus() - focus on a DOM element', function () { - // https://on.cypress.io/focus - cy.get('.action-focus').focus() - .should('have.class', 'focus') - .prev().should('have.attr', 'style', 'color: orange;') - }) - - it('.blur() - blur off a DOM element', function () { - // https://on.cypress.io/blur - cy.get('.action-blur').type('I\'m about to blur').blur() - .should('have.class', 'error') - .prev().should('have.attr', 'style', 'color: red;') - }) - - it('.clear() - clears an input or textarea element', function () { - // https://on.cypress.io/clear - cy.get('.action-clear').type('We are going to clear this text') - .should('have.value', 'We are going to clear this text') - .clear() - .should('have.value', '') - }) - - it('.submit() - submit a form', function () { - // https://on.cypress.io/submit - cy.get('.action-form') - .find('[type="text"]').type('HALFOFF') - cy.get('.action-form').submit() - .next().should('contain', 'Your form has been submitted!') - }) - - it('.click() - click on a DOM element', function () { - // https://on.cypress.io/click - cy.get('.action-btn').click() - - // You can click on 9 specific positions of an element: - // ----------------------------------- - // | topLeft top topRight | - // | | - // | | - // | | - // | left center right | - // | | - // | | - // | | - // | bottomLeft bottom bottomRight | - // ----------------------------------- - - // clicking in the center of the element is the default - cy.get('#action-canvas').click() - - cy.get('#action-canvas').click('topLeft') - cy.get('#action-canvas').click('top') - cy.get('#action-canvas').click('topRight') - cy.get('#action-canvas').click('left') - cy.get('#action-canvas').click('right') - cy.get('#action-canvas').click('bottomLeft') - cy.get('#action-canvas').click('bottom') - cy.get('#action-canvas').click('bottomRight') - - // .click() accepts an x and y coordinate - // that controls where the click occurs :) - - cy.get('#action-canvas') - .click(80, 75) // click 80px on x coord and 75px on y coord - .click(170, 75) - .click(80, 165) - .click(100, 185) - .click(125, 190) - .click(150, 185) - .click(170, 165) - - // click multiple elements by passing multiple: true - cy.get('.action-labels>.label').click({ multiple: true }) - - // Ignore error checking prior to clicking - // like whether the element is visible, clickable or disabled - // this button below is covered by another element. - cy.get('.action-opacity>.btn').click({ force: true }) - }) - - it('.dblclick() - double click on a DOM element', function () { - // Our app has a listener on 'dblclick' event in our 'scripts.js' - // that hides the div and shows an input on double click - - // https://on.cypress.io/dblclick - cy.get('.action-div').dblclick().should('not.be.visible') - cy.get('.action-input-hidden').should('be.visible') - }) - - it('cy.check() - check a checkbox or radio element', function () { - // By default, .check() will check all - // matching checkbox or radio elements in succession, one after another - - // https://on.cypress.io/check - cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') - .check().should('be.checked') - - cy.get('.action-radios [type="radio"]').not('[disabled]') - .check().should('be.checked') - - // .check() accepts a value argument - // that checks only checkboxes or radios - // with matching values - cy.get('.action-radios [type="radio"]').check('radio1').should('be.checked') - - // .check() accepts an array of values - // that checks only checkboxes or radios - // with matching values - cy.get('.action-multiple-checkboxes [type="checkbox"]') - .check(['checkbox1', 'checkbox2']).should('be.checked') - - // Ignore error checking prior to checking - // like whether the element is visible, clickable or disabled - // this checkbox below is disabled. - cy.get('.action-checkboxes [disabled]') - .check({ force: true }).should('be.checked') - - cy.get('.action-radios [type="radio"]') - .check('radio3', { force: true }).should('be.checked') - }) - - it('.uncheck() - uncheck a checkbox element', function () { - // By default, .uncheck() will uncheck all matching - // checkbox elements in succession, one after another - - // https://on.cypress.io/uncheck - cy.get('.action-check [type="checkbox"]') - .not('[disabled]') - .uncheck().should('not.be.checked') - - // .uncheck() accepts a value argument - // that unchecks only checkboxes - // with matching values - cy.get('.action-check [type="checkbox"]') - .check('checkbox1') - .uncheck('checkbox1').should('not.be.checked') - - // .uncheck() accepts an array of values - // that unchecks only checkboxes or radios - // with matching values - cy.get('.action-check [type="checkbox"]') - .check(['checkbox1', 'checkbox3']) - .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') - - // Ignore error checking prior to unchecking - // like whether the element is visible, clickable or disabled - // this checkbox below is disabled. - cy.get('.action-check [disabled]') - .uncheck({ force: true }).should('not.be.checked') - }) - - it('.select() - select an option in a <select> element', function () { - // https://on.cypress.io/select - - // Select option with matching text content - cy.get('.action-select').select('apples') - - // Select option with matching value - cy.get('.action-select').select('fr-bananas') - - // Select options with matching text content - cy.get('.action-select-multiple') - .select(['apples', 'oranges', 'bananas']) - - // Select options with matching values - cy.get('.action-select-multiple') - .select(['fr-apples', 'fr-oranges', 'fr-bananas']) - }) - - it('.scrollIntoView() - scroll an element into view', function () { - // https://on.cypress.io/scrollintoview - - // normally all of these buttons are hidden, because they're not within - // the viewable area of their parent (we need to scroll to see them) - cy.get('#scroll-horizontal button') - .should('not.be.visible') - - // scroll the button into view, as if the user had scrolled - cy.get('#scroll-horizontal button').scrollIntoView() - .should('be.visible') - - cy.get('#scroll-vertical button') - .should('not.be.visible') - - // Cypress handles the scroll direction needed - cy.get('#scroll-vertical button').scrollIntoView() - .should('be.visible') - - cy.get('#scroll-both button') - .should('not.be.visible') - - // Cypress knows to scroll to the right and down - cy.get('#scroll-both button').scrollIntoView() - .should('be.visible') - }) - - it('cy.scrollTo() - scroll the window or element to a position', function () { - - // https://on.cypress.io/scrollTo - - // You can scroll to 9 specific positions of an element: - // ----------------------------------- - // | topLeft top topRight | - // | | - // | | - // | | - // | left center right | - // | | - // | | - // | | - // | bottomLeft bottom bottomRight | - // ----------------------------------- - - // if you chain .scrollTo() off of cy, we will - // scroll the entire window - cy.scrollTo('bottom') - - cy.get('#scrollable-horizontal').scrollTo('right') - - // or you can scroll to a specific coordinate: - // (x axis, y axis) in pixels - cy.get('#scrollable-vertical').scrollTo(250, 250) - - // or you can scroll to a specific percentage - // of the (width, height) of the element - cy.get('#scrollable-both').scrollTo('75%', '25%') - - // control the easing of the scroll (default is 'swing') - cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' }) - - // control the duration of the scroll (in ms) - cy.get('#scrollable-both').scrollTo('center', { duration: 2000 }) - }) - - it('.trigger() - trigger an event on a DOM element', function () { - // To interact with a range input (slider), we need to set its value and - // then trigger the appropriate event to signal it has changed - - // Here, we invoke jQuery's val() method to set the value - // and trigger the 'change' event - - // Note that some implementations may rely on the 'input' event, - // which is fired as a user moves the slider, but is not supported - // by some browsers - - // https://on.cypress.io/trigger - cy.get('.trigger-input-range') - .invoke('val', 25) - .trigger('change') - .get('input[type=range]').siblings('p') - .should('have.text', '25') - - // See our example recipes for more examples of using trigger - // https://on.cypress.io/examples - }) - }) - - context('Window', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/window') - }) - - it('cy.window() - get the global window object', function () { - // https://on.cypress.io/window - cy.window().should('have.property', 'top') - }) - - it('cy.document() - get the document object', function () { - // https://on.cypress.io/document - cy.document().should('have.property', 'charset').and('eq', 'UTF-8') - }) - - it('cy.title() - get the title', function () { - // https://on.cypress.io/title - cy.title().should('include', 'Kitchen Sink') - }) - }) - - context('Viewport', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/viewport') - }) - - it('cy.viewport() - set the viewport size and dimension', function () { - - cy.get('#navbar').should('be.visible') - - // https://on.cypress.io/viewport - cy.viewport(320, 480) - - // the navbar should have collapse since our screen is smaller - cy.get('#navbar').should('not.be.visible') - cy.get('.navbar-toggle').should('be.visible').click() - cy.get('.nav').find('a').should('be.visible') - - // lets see what our app looks like on a super large screen - cy.viewport(2999, 2999) - - // cy.viewport() accepts a set of preset sizes - // to easily set the screen to a device's width and height - - // We added a cy.wait() between each viewport change so you can see - // the change otherwise it's a little too fast to see :) - - cy.viewport('macbook-15') - cy.wait(200) - cy.viewport('macbook-13') - cy.wait(200) - cy.viewport('macbook-11') - cy.wait(200) - cy.viewport('ipad-2') - cy.wait(200) - cy.viewport('ipad-mini') - cy.wait(200) - cy.viewport('iphone-6+') - cy.wait(200) - cy.viewport('iphone-6') - cy.wait(200) - cy.viewport('iphone-5') - cy.wait(200) - cy.viewport('iphone-4') - cy.wait(200) - cy.viewport('iphone-3') - cy.wait(200) - - // cy.viewport() accepts an orientation for all presets - // the default orientation is 'portrait' - cy.viewport('ipad-2', 'portrait') - cy.wait(200) - cy.viewport('iphone-4', 'landscape') - cy.wait(200) - - // The viewport will be reset back to the default dimensions - // in between tests (the default is set in cypress.json) - }) - }) - - context('Location', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/location') - }) - - // We look at the url to make assertions - // about the page's state - - it('cy.hash() - get the current URL hash', function () { - // https://on.cypress.io/hash - cy.hash().should('be.empty') - }) - - it('cy.location() - get window.location', function () { - // https://on.cypress.io/location - cy.location().should(function (location) { - expect(location.hash).to.be.empty - expect(location.href).to.eq('https://example.cypress.io/commands/location') - expect(location.host).to.eq('example.cypress.io') - expect(location.hostname).to.eq('example.cypress.io') - expect(location.origin).to.eq('https://example.cypress.io') - expect(location.pathname).to.eq('/commands/location') - expect(location.port).to.eq('') - expect(location.protocol).to.eq('https:') - expect(location.search).to.be.empty - }) - }) - - it('cy.url() - get the current URL', function () { - // https://on.cypress.io/url - cy.url().should('eq', 'https://example.cypress.io/commands/location') - }) - }) - - context('Navigation', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io') - cy.get('.navbar-nav').contains('Commands').click() - cy.get('.dropdown-menu').contains('Navigation').click() - }) - - it('cy.go() - go back or forward in the browser\'s history', function () { - cy.location('pathname').should('include', 'navigation') - - // https://on.cypress.io/go - cy.go('back') - cy.location('pathname').should('not.include', 'navigation') - - cy.go('forward') - cy.location('pathname').should('include', 'navigation') - - // equivalent to clicking back - cy.go(-1) - cy.location('pathname').should('not.include', 'navigation') - - // equivalent to clicking forward - cy.go(1) - cy.location('pathname').should('include', 'navigation') - }) - - it('cy.reload() - reload the page', function () { - // https://on.cypress.io/reload - cy.reload() - - // reload the page without using the cache - cy.reload(true) - }) - - it('cy.visit() - visit a remote url', function () { - // Visit any sub-domain of your current domain - // https://on.cypress.io/visit - - // Pass options to the visit - cy.visit('https://example.cypress.io/commands/navigation', { - timeout: 50000, // increase total time for the visit to resolve - onBeforeLoad (contentWindow) { - // contentWindow is the remote page's window object - }, - onLoad (contentWindow) { - // contentWindow is the remote page's window object - }, - }) - }) - }) - - context('Assertions', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/assertions') - }) - - describe('Implicit Assertions', function () { - - it('.should() - make an assertion about the current subject', function () { - // https://on.cypress.io/should - cy.get('.assertion-table') - .find('tbody tr:last').should('have.class', 'success') - }) - - it('.and() - chain multiple assertions together', function () { - // https://on.cypress.io/and - cy.get('.assertions-link') - .should('have.class', 'active') - .and('have.attr', 'href') - .and('include', 'cypress.io') - }) - }) - - describe('Explicit Assertions', function () { - // https://on.cypress.io/assertions - it('expect - assert shape of an object', function () { - const person = { - name: 'Joe', - age: 20, - } - expect(person).to.have.all.keys('name', 'age') - }) - - it('expect - make an assertion about a specified subject', function () { - // We can use Chai's BDD style assertions - expect(true).to.be.true - - // Pass a function to should that can have any number - // of explicit assertions within it. - cy.get('.assertions-p').find('p') - .should(function ($p) { - // return an array of texts from all of the p's - let texts = $p.map(function (i, el) { - // https://on.cypress.io/$ - return Cypress.$(el).text() - }) - - // jquery map returns jquery object - // and .get() convert this to simple array - texts = texts.get() - - // array should have length of 3 - expect(texts).to.have.length(3) - - // set this specific subject - expect(texts).to.deep.eq([ - 'Some text from first p', - 'More text from second p', - 'And even more text from third p', - ]) - }) - }) - }) - }) - - context('Misc', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/misc') - }) - - it('.end() - end the command chain', function () { - // cy.end is useful when you want to end a chain of commands - // and force Cypress to re-query from the root element - - // https://on.cypress.io/end - cy.get('.misc-table').within(function () { - // ends the current chain and yields null - cy.contains('Cheryl').click().end() - - // queries the entire table again - cy.contains('Charles').click() - }) - }) - - it('cy.exec() - execute a system command', function () { - // cy.exec allows you to execute a system command. - // so you can take actions necessary for your test, - // but outside the scope of Cypress. - - // https://on.cypress.io/exec - cy.exec('echo Jane Lane') - .its('stdout').should('contain', 'Jane Lane') - - // we can use Cypress.platform string to - // select appropriate command - // https://on.cypress/io/platform - cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) - - if (Cypress.platform === 'win32') { - cy.exec('print cypress.json') - .its('stderr').should('be.empty') - } else { - cy.exec('cat cypress.json') - .its('stderr').should('be.empty') - - cy.exec('pwd') - .its('code').should('eq', 0) - } - }) - - it('cy.focused() - get the DOM element that has focus', function () { - // https://on.cypress.io/focused - cy.get('.misc-form').find('#name').click() - cy.focused().should('have.id', 'name') - - cy.get('.misc-form').find('#description').click() - cy.focused().should('have.id', 'description') - }) - - it('cy.screenshot() - take a screenshot', function () { - // https://on.cypress.io/screenshot - cy.screenshot('my-image') - }) - - it('cy.wrap() - wrap an object', function () { - // https://on.cypress.io/wrap - cy.wrap({ foo: 'bar' }) - .should('have.property', 'foo') - .and('include', 'bar') - }) - }) - - context('Connectors', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/connectors') - }) - - it('.each() - iterate over an array of elements', function () { - // https://on.cypress.io/each - cy.get('.connectors-each-ul>li') - .each(function ($el, index, $list) { - console.log($el, index, $list) - }) - }) - - it('.its() - get properties on the current subject', function () { - // https://on.cypress.io/its - cy.get('.connectors-its-ul>li') - // calls the 'length' property yielding that value - .its('length') - .should('be.gt', 2) - }) - - it('.invoke() - invoke a function on the current subject', function () { - // our div is hidden in our script.js - // $('.connectors-div').hide() - - // https://on.cypress.io/invoke - cy.get('.connectors-div').should('be.hidden') - - // call the jquery method 'show' on the 'div.container' - .invoke('show') - .should('be.visible') - }) - - it('.spread() - spread an array as individual args to callback function', function () { - // https://on.cypress.io/spread - let arr = ['foo', 'bar', 'baz'] - - cy.wrap(arr).spread(function (foo, bar, baz) { - expect(foo).to.eq('foo') - expect(bar).to.eq('bar') - expect(baz).to.eq('baz') - }) - }) - - it('.then() - invoke a callback function with the current subject', function () { - // https://on.cypress.io/then - cy.get('.connectors-list>li').then(function ($lis) { - expect($lis).to.have.length(3) - expect($lis.eq(0)).to.contain('Walk the dog') - expect($lis.eq(1)).to.contain('Feed the cat') - expect($lis.eq(2)).to.contain('Write JavaScript') - }) - }) - }) - - context('Aliasing', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/aliasing') - }) - - // We alias a DOM element for use later - // We don't have to traverse to the element - // later in our code, we just reference it with @ - - it('.as() - alias a route or DOM element for later use', function () { - // this is a good use case for an alias, - // we don't want to write this long traversal again - - // https://on.cypress.io/as - cy.get('.as-table').find('tbody>tr') - .first().find('td').first().find('button').as('firstBtn') - - // maybe do some more testing here... - - // when we reference the alias, we place an - // @ in front of it's name - cy.get('@firstBtn').click() - - cy.get('@firstBtn') - .should('have.class', 'btn-success') - .and('contain', 'Changed') - }) - }) - - context('Waiting', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/waiting') - }) - // BE CAREFUL of adding unnecessary wait times. - - // https://on.cypress.io/wait - it('cy.wait() - wait for a specific amount of time', function () { - cy.get('.wait-input1').type('Wait 1000ms after typing') - cy.wait(1000) - cy.get('.wait-input2').type('Wait 1000ms after typing') - cy.wait(1000) - cy.get('.wait-input3').type('Wait 1000ms after typing') - cy.wait(1000) - }) - - // Waiting for a specific resource to resolve - // is covered within the cy.route() test below - }) - - context('Network Requests', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/network-requests') - }) - - // Manage AJAX / XHR requests in your app - - it('cy.server() - control behavior of network requests and responses', function () { - // https://on.cypress.io/server - cy.server().should(function (server) { - // the default options on server - // you can override any of these options - expect(server.delay).to.eq(0) - expect(server.method).to.eq('GET') - expect(server.status).to.eq(200) - expect(server.headers).to.be.null - expect(server.response).to.be.null - expect(server.onRequest).to.be.undefined - expect(server.onResponse).to.be.undefined - expect(server.onAbort).to.be.undefined - - // These options control the server behavior - // affecting all requests - - // pass false to disable existing route stubs - expect(server.enable).to.be.true - // forces requests that don't match your routes to 404 - expect(server.force404).to.be.false - // whitelists requests from ever being logged or stubbed - expect(server.whitelist).to.be.a('function') - }) - - cy.server({ - method: 'POST', - delay: 1000, - status: 422, - response: {}, - }) - - // any route commands will now inherit the above options - // from the server. anything we pass specifically - // to route will override the defaults though. - }) - - it('cy.request() - make an XHR request', function () { - // https://on.cypress.io/request - cy.request('https://jsonplaceholder.typicode.com/comments') - .should(function (response) { - expect(response.status).to.eq(200) - expect(response.body).to.have.length(500) - expect(response).to.have.property('headers') - expect(response).to.have.property('duration') - }) - }) - - it('cy.route() - route responses to matching requests', function () { - let message = 'whoa, this comment doesn\'t exist' - cy.server() - - // **** GET comments route **** - - // https://on.cypress.io/route - cy.route(/comments\/1/).as('getComment') - - // we have code that fetches a comment when - // the button is clicked in scripts.js - cy.get('.network-btn').click() - - // **** Wait **** - - // Wait for a specific resource to resolve - // continuing to the next command - - // https://on.cypress.io/wait - cy.wait('@getComment').its('status').should('eq', 200) - - // **** POST comment route **** - - // Specify the route to listen to method 'POST' - cy.route('POST', '/comments').as('postComment') - - // we have code that posts a comment when - // the button is clicked in scripts.js - cy.get('.network-post').click() - cy.wait('@postComment') - - // get the route - cy.get('@postComment').then(function (xhr) { - expect(xhr.requestBody).to.include('email') - expect(xhr.requestHeaders).to.have.property('Content-Type') - expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()') - }) - - // **** Stubbed PUT comment route **** - cy.route({ - method: 'PUT', - url: /comments\/\d+/, - status: 404, - response: { error: message }, - delay: 500, - }).as('putComment') - - // we have code that puts a comment when - // the button is clicked in scripts.js - cy.get('.network-put').click() - - cy.wait('@putComment') - - // our 404 statusCode logic in scripts.js executed - cy.get('.network-put-comment').should('contain', message) - }) - }) - - context('Files', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/files') - }) - it('cy.fixture() - load a fixture', function () { - // Instead of writing a response inline you can - // connect a response with a fixture file - // located in fixtures folder. - - cy.server() - - // https://on.cypress.io/fixture - cy.fixture('example.json').as('comment') - - cy.route(/comments/, '@comment').as('getComment') - - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.fixture-btn').click() - - cy.wait('@getComment').its('responseBody') - .should('have.property', 'name') - .and('include', 'Using fixtures to represent data') - - // you can also just write the fixture in the route - cy.route(/comments/, 'fixture:example.json').as('getComment') - - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.fixture-btn').click() - - cy.wait('@getComment').its('responseBody') - .should('have.property', 'name') - .and('include', 'Using fixtures to represent data') - - // or write fx to represent fixture - // by default it assumes it's .json - cy.route(/comments/, 'fx:example').as('getComment') - - // we have code that gets a comment when - // the button is clicked in scripts.js - cy.get('.fixture-btn').click() - - cy.wait('@getComment').its('responseBody') - .should('have.property', 'name') - .and('include', 'Using fixtures to represent data') - }) - - it('cy.readFile() - read a files contents', function () { - // You can read a file and yield its contents - // The filePath is relative to your project's root. - - // https://on.cypress.io/readfile - cy.readFile('cypress.json').then(function (json) { - expect(json).to.be.an('object') - }) - - }) - - it('cy.writeFile() - write to a file', function () { - // You can write to a file with the specified contents - - // Use a response from a request to automatically - // generate a fixture file for use later - cy.request('https://jsonplaceholder.typicode.com/users') - .then(function (response) { - // https://on.cypress.io/writefile - cy.writeFile('cypress/fixtures/users.json', response.body) - }) - cy.fixture('users').should(function (users) { - expect(users[0].name).to.exist - }) - - // JavaScript arrays and objects are stringified and formatted into text. - cy.writeFile('cypress/fixtures/profile.json', { - id: 8739, - name: 'Jane', - email: 'jane@example.com', - }) - - cy.fixture('profile').should(function (profile) { - expect(profile.name).to.eq('Jane') - }) - }) - }) - - context('Local Storage', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/commands/local-storage') - }) - // Although local storage is automatically cleared - // to maintain a clean state in between tests - // sometimes we need to clear the local storage manually - - it('cy.clearLocalStorage() - clear all data in local storage', function () { - // https://on.cypress.io/clearlocalstorage - cy.get('.ls-btn').click().should(function () { - expect(localStorage.getItem('prop1')).to.eq('red') - expect(localStorage.getItem('prop2')).to.eq('blue') - expect(localStorage.getItem('prop3')).to.eq('magenta') - }) - - // clearLocalStorage() yields the localStorage object - cy.clearLocalStorage().should(function (ls) { - expect(ls.getItem('prop1')).to.be.null - expect(ls.getItem('prop2')).to.be.null - expect(ls.getItem('prop3')).to.be.null - }) - - // **** Clear key matching string in Local Storage **** - cy.get('.ls-btn').click().should(function () { - expect(localStorage.getItem('prop1')).to.eq('red') - expect(localStorage.getItem('prop2')).to.eq('blue') - expect(localStorage.getItem('prop3')).to.eq('magenta') - }) - - cy.clearLocalStorage('prop1').should(function (ls) { - expect(ls.getItem('prop1')).to.be.null - expect(ls.getItem('prop2')).to.eq('blue') - expect(ls.getItem('prop3')).to.eq('magenta') - }) - - // **** Clear key's matching regex in Local Storage **** - cy.get('.ls-btn').click().should(function () { - expect(localStorage.getItem('prop1')).to.eq('red') - expect(localStorage.getItem('prop2')).to.eq('blue') - expect(localStorage.getItem('prop3')).to.eq('magenta') - }) - - cy.clearLocalStorage(/prop1|2/).should(function (ls) { - expect(ls.getItem('prop1')).to.be.null - expect(ls.getItem('prop2')).to.be.null - expect(ls.getItem('prop3')).to.eq('magenta') - }) - }) - }) - - context('Cookies', function () { - beforeEach(function () { - Cypress.Cookies.debug(true) - - cy.visit('https://example.cypress.io/commands/cookies') - - // clear cookies again after visiting to remove - // any 3rd party cookies picked up such as cloudflare - cy.clearCookies() - }) - - it('cy.getCookie() - get a browser cookie', function () { - // https://on.cypress.io/getcookie - cy.get('#getCookie .set-a-cookie').click() - - // cy.getCookie() yields a cookie object - cy.getCookie('token').should('have.property', 'value', '123ABC') - }) - - it('cy.getCookies() - get browser cookies', function () { - // https://on.cypress.io/getcookies - cy.getCookies().should('be.empty') - - cy.get('#getCookies .set-a-cookie').click() - - // cy.getCookies() yields an array of cookies - cy.getCookies().should('have.length', 1).should(function (cookies) { - - // each cookie has these properties - expect(cookies[0]).to.have.property('name', 'token') - expect(cookies[0]).to.have.property('value', '123ABC') - expect(cookies[0]).to.have.property('httpOnly', false) - expect(cookies[0]).to.have.property('secure', false) - expect(cookies[0]).to.have.property('domain') - expect(cookies[0]).to.have.property('path') - }) - }) - - it('cy.setCookie() - set a browser cookie', function () { - // https://on.cypress.io/setcookie - cy.getCookies().should('be.empty') - - cy.setCookie('foo', 'bar') - - // cy.getCookie() yields a cookie object - cy.getCookie('foo').should('have.property', 'value', 'bar') - }) - - it('cy.clearCookie() - clear a browser cookie', function () { - // https://on.cypress.io/clearcookie - cy.getCookie('token').should('be.null') - - cy.get('#clearCookie .set-a-cookie').click() - - cy.getCookie('token').should('have.property', 'value', '123ABC') - - // cy.clearCookies() yields null - cy.clearCookie('token').should('be.null') - - cy.getCookie('token').should('be.null') - }) - - it('cy.clearCookies() - clear browser cookies', function () { - // https://on.cypress.io/clearcookies - cy.getCookies().should('be.empty') - - cy.get('#clearCookies .set-a-cookie').click() - - cy.getCookies().should('have.length', 1) - - // cy.clearCookies() yields null - cy.clearCookies() - - cy.getCookies().should('be.empty') - }) - }) - - context('Spies, Stubs, and Clock', function () { - it('cy.spy() - wrap a method in a spy', function () { - // https://on.cypress.io/spy - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') - - let obj = { - foo () {}, - } - - let spy = cy.spy(obj, 'foo').as('anyArgs') - - obj.foo() - - expect(spy).to.be.called - - }) - - it('cy.stub() - create a stub and/or replace a function with a stub', function () { - // https://on.cypress.io/stub - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') - - let obj = { - foo () {}, - } - - let stub = cy.stub(obj, 'foo').as('foo') - - obj.foo('foo', 'bar') - - expect(stub).to.be.called - - }) - - it('cy.clock() - control time in the browser', function () { - // create the date in UTC so its always the same - // no matter what local timezone the browser is running in - let now = new Date(Date.UTC(2017, 2, 14)).getTime() - - // https://on.cypress.io/clock - cy.clock(now) - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') - cy.get('#clock-div').click() - .should('have.text', '1489449600') - }) - - it('cy.tick() - move time in the browser', function () { - // create the date in UTC so its always the same - // no matter what local timezone the browser is running in - let now = new Date(Date.UTC(2017, 2, 14)).getTime() - - // https://on.cypress.io/tick - cy.clock(now) - cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') - cy.get('#tick-div').click() - .should('have.text', '1489449600') - cy.tick(10000) // 10 seconds passed - cy.get('#tick-div').click() - .should('have.text', '1489449610') - }) - }) - - context('Utilities', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/utilities') - }) - - it('Cypress._.method() - call a lodash method', function () { - // use the _.chain, _.map, _.take, and _.value functions - // https://on.cypress.io/_ - cy.request('https://jsonplaceholder.typicode.com/users') - .then(function (response) { - let ids = Cypress._.chain(response.body).map('id').take(3).value() - - expect(ids).to.deep.eq([1, 2, 3]) - }) - }) - - it('Cypress.$(selector) - call a jQuery method', function () { - // https://on.cypress.io/$ - let $li = Cypress.$('.utility-jquery li:first') - - cy.wrap($li) - .should('not.have.class', 'active') - .click() - .should('have.class', 'active') - }) - - it('Cypress.moment() - format or parse dates using a moment method', function () { - // use moment's format function - // https://on.cypress.io/cypress-moment - let time = Cypress.moment().utc('2014-04-25T19:38:53.196Z').format('h:mm A') - - cy.get('.utility-moment').contains('3:38 PM') - .should('have.class', 'badge') - }) - - it('Cypress.Blob.method() - blob utilities and base64 string conversion', function () { - cy.get('.utility-blob').then(function ($div) { - // https://on.cypress.io/blob - // https://github.com/nolanlawson/blob-util#imgSrcToDataURL - // get the dataUrl string for the javascript-logo - return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') - .then(function (dataUrl) { - // create an <img> element and set its src to the dataUrl - let img = Cypress.$('<img />', { src: dataUrl }) - // need to explicitly return cy here since we are initially returning - // the Cypress.Blob.imgSrcToDataURL promise to our test - // append the image - $div.append(img) - - cy.get('.utility-blob img').click() - .should('have.attr', 'src', dataUrl) - }) - }) - }) - - it('new Cypress.Promise(function) - instantiate a bluebird promise', function () { - // https://on.cypress.io/promise - let waited = false - - function waitOneSecond () { - // return a promise that resolves after 1 second - return new Cypress.Promise(function (resolve, reject) { - setTimeout(function () { - // set waited to true - waited = true - - // resolve with 'foo' string - resolve('foo') - }, 1000) - }) - } - - cy.then(function () { - // return a promise to cy.then() that - // is awaited until it resolves - return waitOneSecond().then(function (str) { - expect(str).to.eq('foo') - expect(waited).to.be.true - }) - }) - }) - }) - - - context('Cypress.config()', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/cypress-api/config') - }) - - it('Cypress.config() - get and set configuration options', function () { - // https://on.cypress.io/config - let myConfig = Cypress.config() - - expect(myConfig).to.have.property('animationDistanceThreshold', 5) - expect(myConfig).to.have.property('baseUrl', null) - expect(myConfig).to.have.property('defaultCommandTimeout', 4000) - expect(myConfig).to.have.property('requestTimeout', 5000) - expect(myConfig).to.have.property('responseTimeout', 30000) - expect(myConfig).to.have.property('viewportHeight', 660) - expect(myConfig).to.have.property('viewportWidth', 1000) - expect(myConfig).to.have.property('pageLoadTimeout', 60000) - expect(myConfig).to.have.property('waitForAnimations', true) - - expect(Cypress.config('pageLoadTimeout')).to.eq(60000) - - // this will change the config for the rest of your tests! - Cypress.config('pageLoadTimeout', 20000) - - expect(Cypress.config('pageLoadTimeout')).to.eq(20000) - - Cypress.config('pageLoadTimeout', 60000) - }) - }) - - context('Cypress.env()', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/cypress-api/env') - }) - - // We can set environment variables for highly dynamic values - - // https://on.cypress.io/environment-variables - it('Cypress.env() - get environment variables', function () { - // https://on.cypress.io/env - // set multiple environment variables - Cypress.env({ - host: 'veronica.dev.local', - api_server: 'http://localhost:8888/v1/', - }) - - // get environment variable - expect(Cypress.env('host')).to.eq('veronica.dev.local') - - // set environment variable - Cypress.env('api_server', 'http://localhost:8888/v2/') - expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') - - // get all environment variable - expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') - expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') - }) - }) - - context('Cypress.Cookies', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/cypress-api/cookies') - }) - - // https://on.cypress.io/cookies - it('Cypress.Cookies.debug() - enable or disable debugging', function () { - Cypress.Cookies.debug(true) - - // Cypress will now log in the console when - // cookies are set or cleared - cy.setCookie('fakeCookie', '123ABC') - cy.clearCookie('fakeCookie') - cy.setCookie('fakeCookie', '123ABC') - cy.clearCookie('fakeCookie') - cy.setCookie('fakeCookie', '123ABC') - }) - - it('Cypress.Cookies.preserveOnce() - preserve cookies by key', function () { - // normally cookies are reset after each test - cy.getCookie('fakeCookie').should('not.be.ok') - - // preserving a cookie will not clear it when - // the next test starts - cy.setCookie('lastCookie', '789XYZ') - Cypress.Cookies.preserveOnce('lastCookie') - }) - - it('Cypress.Cookies.defaults() - set defaults for all cookies', function () { - // now any cookie with the name 'session_id' will - // not be cleared before each new test runs - Cypress.Cookies.defaults({ - whitelist: 'session_id', - }) - }) - }) - - context('Cypress.dom', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/cypress-api/dom') - }) - - // https://on.cypress.io/dom - it('Cypress.dom.isHidden() - determine if a DOM element is hidden', function () { - let hiddenP = Cypress.$('.dom-p p.hidden').get(0) - let visibleP = Cypress.$('.dom-p p.visible').get(0) - - // our first paragraph has css class 'hidden' - expect(Cypress.dom.isHidden(hiddenP)).to.be.true - expect(Cypress.dom.isHidden(visibleP)).to.be.false - }) - }) - - context('Cypress.Server', function () { - beforeEach(function () { - cy.visit('https://example.cypress.io/cypress-api/server') - }) - - // Permanently override server options for - // all instances of cy.server() - - // https://on.cypress.io/cypress-server - it('Cypress.Server.defaults() - change default config of server', function () { - Cypress.Server.defaults({ - delay: 0, - force404: false, - whitelist (xhr) { - // handle custom logic for whitelisting - }, - }) - }) - }) -}) diff --git a/packages/example/cypress/integration/examples/actions.spec.js b/packages/example/cypress/integration/examples/actions.spec.js new file mode 100644 index 000000000000..0375aba7823c --- /dev/null +++ b/packages/example/cypress/integration/examples/actions.spec.js @@ -0,0 +1,272 @@ +/// <reference types="Cypress" /> + +context('Actions', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/actions') + }) + + // https://on.cypress.io/interacting-with-elements + + it('.type() - type into a DOM element', () => { + // https://on.cypress.io/type + cy.get('.action-email') + .type('fake@email.com').should('have.value', 'fake@email.com') + + // .type() with special character sequences + .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') + .type('{del}{selectall}{backspace}') + + // .type() with key modifiers + .type('{alt}{option}') //these are equivalent + .type('{ctrl}{control}') //these are equivalent + .type('{meta}{command}{cmd}') //these are equivalent + .type('{shift}') + + // Delay each keypress by 0.1 sec + .type('slow.typing@email.com', { delay: 100 }) + .should('have.value', 'slow.typing@email.com') + + cy.get('.action-disabled') + // Ignore error checking prior to type + // like whether the input is visible or disabled + .type('disabled error checking', { force: true }) + .should('have.value', 'disabled error checking') + }) + + it('.focus() - focus on a DOM element', () => { + // https://on.cypress.io/focus + cy.get('.action-focus').focus() + .should('have.class', 'focus') + .prev().should('have.attr', 'style', 'color: orange;') + }) + + it('.blur() - blur off a DOM element', () => { + // https://on.cypress.io/blur + cy.get('.action-blur').type('About to blur').blur() + .should('have.class', 'error') + .prev().should('have.attr', 'style', 'color: red;') + }) + + it('.clear() - clears an input or textarea element', () => { + // https://on.cypress.io/clear + cy.get('.action-clear').type('Clear this text') + .should('have.value', 'Clear this text') + .clear() + .should('have.value', '') + }) + + it('.submit() - submit a form', () => { + // https://on.cypress.io/submit + cy.get('.action-form') + .find('[type="text"]').type('HALFOFF') + cy.get('.action-form').submit() + .next().should('contain', 'Your form has been submitted!') + }) + + it('.click() - click on a DOM element', () => { + // https://on.cypress.io/click + cy.get('.action-btn').click() + + // You can click on 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- + + // clicking in the center of the element is the default + cy.get('#action-canvas').click() + + cy.get('#action-canvas').click('topLeft') + cy.get('#action-canvas').click('top') + cy.get('#action-canvas').click('topRight') + cy.get('#action-canvas').click('left') + cy.get('#action-canvas').click('right') + cy.get('#action-canvas').click('bottomLeft') + cy.get('#action-canvas').click('bottom') + cy.get('#action-canvas').click('bottomRight') + + // .click() accepts an x and y coordinate + // that controls where the click occurs :) + + cy.get('#action-canvas') + .click(80, 75) // click 80px on x coord and 75px on y coord + .click(170, 75) + .click(80, 165) + .click(100, 185) + .click(125, 190) + .click(150, 185) + .click(170, 165) + + // click multiple elements by passing multiple: true + cy.get('.action-labels>.label').click({ multiple: true }) + + // Ignore error checking prior to clicking + cy.get('.action-opacity>.btn').click({ force: true }) + }) + + it('.dblclick() - double click on a DOM element', () => { + // https://on.cypress.io/dblclick + + // Our app has a listener on 'dblclick' event in our 'scripts.js' + // that hides the div and shows an input on double click + cy.get('.action-div').dblclick().should('not.be.visible') + cy.get('.action-input-hidden').should('be.visible') + }) + + it('.check() - check a checkbox or radio element', () => { + // https://on.cypress.io/check + + // By default, .check() will check all + // matching checkbox or radio elements in succession, one after another + cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') + .check().should('be.checked') + + cy.get('.action-radios [type="radio"]').not('[disabled]') + .check().should('be.checked') + + // .check() accepts a value argument + cy.get('.action-radios [type="radio"]') + .check('radio1').should('be.checked') + + // .check() accepts an array of values + cy.get('.action-multiple-checkboxes [type="checkbox"]') + .check(['checkbox1', 'checkbox2']).should('be.checked') + + // Ignore error checking prior to checking + cy.get('.action-checkboxes [disabled]') + .check({ force: true }).should('be.checked') + + cy.get('.action-radios [type="radio"]') + .check('radio3', { force: true }).should('be.checked') + }) + + it('.uncheck() - uncheck a checkbox element', () => { + // https://on.cypress.io/uncheck + + // By default, .uncheck() will uncheck all matching + // checkbox elements in succession, one after another + cy.get('.action-check [type="checkbox"]') + .not('[disabled]') + .uncheck().should('not.be.checked') + + // .uncheck() accepts a value argument + cy.get('.action-check [type="checkbox"]') + .check('checkbox1') + .uncheck('checkbox1').should('not.be.checked') + + // .uncheck() accepts an array of values + cy.get('.action-check [type="checkbox"]') + .check(['checkbox1', 'checkbox3']) + .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') + + // Ignore error checking prior to unchecking + cy.get('.action-check [disabled]') + .uncheck({ force: true }).should('not.be.checked') + }) + + it('.select() - select an option in a <select> element', () => { + // https://on.cypress.io/select + + // Select option(s) with matching text content + cy.get('.action-select').select('apples') + + cy.get('.action-select-multiple') + .select(['apples', 'oranges', 'bananas']) + + // Select option(s) with matching value + cy.get('.action-select').select('fr-bananas') + + cy.get('.action-select-multiple') + .select(['fr-apples', 'fr-oranges', 'fr-bananas']) + }) + + it('.scrollIntoView() - scroll an element into view', () => { + // https://on.cypress.io/scrollintoview + + // normally all of these buttons are hidden, + // because they're not within + // the viewable area of their parent + // (we need to scroll to see them) + cy.get('#scroll-horizontal button') + .should('not.be.visible') + + // scroll the button into view, as if the user had scrolled + cy.get('#scroll-horizontal button').scrollIntoView() + .should('be.visible') + + cy.get('#scroll-vertical button') + .should('not.be.visible') + + // Cypress handles the scroll direction needed + cy.get('#scroll-vertical button').scrollIntoView() + .should('be.visible') + + cy.get('#scroll-both button') + .should('not.be.visible') + + // Cypress knows to scroll to the right and down + cy.get('#scroll-both button').scrollIntoView() + .should('be.visible') + }) + + it('cy.scrollTo() - scroll the window or element to a position', () => { + + // https://on.cypress.io/scrollTo + + // You can scroll to 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- + + // if you chain .scrollTo() off of cy, we will + // scroll the entire window + cy.scrollTo('bottom') + + cy.get('#scrollable-horizontal').scrollTo('right') + + // or you can scroll to a specific coordinate: + // (x axis, y axis) in pixels + cy.get('#scrollable-vertical').scrollTo(250, 250) + + // or you can scroll to a specific percentage + // of the (width, height) of the element + cy.get('#scrollable-both').scrollTo('75%', '25%') + + // control the easing of the scroll (default is 'swing') + cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' }) + + // control the duration of the scroll (in ms) + cy.get('#scrollable-both').scrollTo('center', { duration: 2000 }) + }) + + it('.trigger() - trigger an event on a DOM element', () => { + // https://on.cypress.io/trigger + + // To interact with a range input (slider) + // we need to set its value & trigger the + // event to signal it changed + + // Here, we invoke jQuery's val() method to set + // the value and trigger the 'change' event + cy.get('.trigger-input-range') + .invoke('val', 25) + .trigger('change') + .get('input[type=range]').siblings('p') + .should('have.text', '25') + }) +}) diff --git a/packages/example/cypress/integration/examples/aliasing.spec.js b/packages/example/cypress/integration/examples/aliasing.spec.js new file mode 100644 index 000000000000..95bac735c44b --- /dev/null +++ b/packages/example/cypress/integration/examples/aliasing.spec.js @@ -0,0 +1,42 @@ +/// <reference types="Cypress" /> + +context('Aliasing', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/aliasing') + }) + + it('.as() - alias a DOM element for later use', () => { + // https://on.cypress.io/as + + // Alias a DOM element for use later + // We don't have to traverse to the element + // later in our code, we reference it with @ + + cy.get('.as-table').find('tbody>tr') + .first().find('td').first() + .find('button').as('firstBtn') + + // when we reference the alias, we place an + // @ in front of its name + cy.get('@firstBtn').click() + + cy.get('@firstBtn') + .should('have.class', 'btn-success') + .and('contain', 'Changed') + }) + + it('.as() - alias a route for later use', () => { + + // Alias the route to wait for its response + cy.server() + cy.route('GET', 'comments/*').as('getComment') + + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get('.network-btn').click() + + // https://on.cypress.io/wait + cy.wait('@getComment').its('status').should('eq', 200) + + }) +}) diff --git a/packages/example/cypress/integration/examples/assertions.spec.js b/packages/example/cypress/integration/examples/assertions.spec.js new file mode 100644 index 000000000000..c9c25c223bb5 --- /dev/null +++ b/packages/example/cypress/integration/examples/assertions.spec.js @@ -0,0 +1,63 @@ +/// <reference types="Cypress" /> + +context('Assertions', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/assertions') + }) + + describe('Implicit Assertions', () => { + + it('.should() - make an assertion about the current subject', () => { + // https://on.cypress.io/should + cy.get('.assertion-table') + .find('tbody tr:last').should('have.class', 'success') + }) + + it('.and() - chain multiple assertions together', () => { + // https://on.cypress.io/and + cy.get('.assertions-link') + .should('have.class', 'active') + .and('have.attr', 'href') + .and('include', 'cypress.io') + }) + }) + + describe('Explicit Assertions', () => { + // https://on.cypress.io/assertions + it('expect - make an assertion about a specified subject', () => { + // We can use Chai's BDD style assertions + expect(true).to.be.true + + // Pass a function to should that can have any number + // of explicit assertions within it. + cy.get('.assertions-p').find('p') + .should(($p) => { + // return an array of texts from all of the p's + let texts = $p.map((i, el) => // https://on.cypress.io/$ + Cypress.$(el).text()) + + // jquery map returns jquery object + // and .get() convert this to simple array + texts = texts.get() + + // array should have length of 3 + expect(texts).to.have.length(3) + + // set this specific subject + expect(texts).to.deep.eq([ + 'Some text from first p', + 'More text from second p', + 'And even more text from third p', + ]) + }) + }) + + it('assert - assert shape of an object', () => { + const person = { + name: 'Joe', + age: 20, + } + assert.isObject(person, 'value is object') + }) + }) +}) diff --git a/packages/example/cypress/integration/examples/connectors.spec.js b/packages/example/cypress/integration/examples/connectors.spec.js new file mode 100644 index 000000000000..97c03bb328b4 --- /dev/null +++ b/packages/example/cypress/integration/examples/connectors.spec.js @@ -0,0 +1,55 @@ +/// <reference types="Cypress" /> + +context('Connectors', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/connectors') + }) + + it('.each() - iterate over an array of elements', () => { + // https://on.cypress.io/each + cy.get('.connectors-each-ul>li') + .each(($el, index, $list) => { + console.log($el, index, $list) + }) + }) + + it('.its() - get properties on the current subject', () => { + // https://on.cypress.io/its + cy.get('.connectors-its-ul>li') + // calls the 'length' property yielding that value + .its('length') + .should('be.gt', 2) + }) + + it('.invoke() - invoke a function on the current subject', () => { + // our div is hidden in our script.js + // $('.connectors-div').hide() + + // https://on.cypress.io/invoke + cy.get('.connectors-div').should('be.hidden') + // call the jquery method 'show' on the 'div.container' + .invoke('show') + .should('be.visible') + }) + + it('.spread() - spread an array as individual args to callback function', () => { + // https://on.cypress.io/spread + const arr = ['foo', 'bar', 'baz'] + + cy.wrap(arr).spread((foo, bar, baz) => { + expect(foo).to.eq('foo') + expect(bar).to.eq('bar') + expect(baz).to.eq('baz') + }) + }) + + it('.then() - invoke a callback function with the current subject', () => { + // https://on.cypress.io/then + cy.get('.connectors-list>li').then(($lis) => { + expect($lis).to.have.length(3) + expect($lis.eq(0)).to.contain('Walk the dog') + expect($lis.eq(1)).to.contain('Feed the cat') + expect($lis.eq(2)).to.contain('Write JavaScript') + }) + }) +}) diff --git a/packages/example/cypress/integration/examples/cookies.spec.js b/packages/example/cypress/integration/examples/cookies.spec.js new file mode 100644 index 000000000000..bb540e95eb0a --- /dev/null +++ b/packages/example/cypress/integration/examples/cookies.spec.js @@ -0,0 +1,78 @@ +/// <reference types="Cypress" /> + +context('Cookies', () => { + beforeEach(() => { + Cypress.Cookies.debug(true) + + cy.visit('https://example.cypress.io/commands/cookies') + + // clear cookies again after visiting to remove + // any 3rd party cookies picked up such as cloudflare + cy.clearCookies() + }) + + it('cy.getCookie() - get a browser cookie', () => { + // https://on.cypress.io/getcookie + cy.get('#getCookie .set-a-cookie').click() + + // cy.getCookie() yields a cookie object + cy.getCookie('token').should('have.property', 'value', '123ABC') + }) + + it('cy.getCookies() - get browser cookies', () => { + // https://on.cypress.io/getcookies + cy.getCookies().should('be.empty') + + cy.get('#getCookies .set-a-cookie').click() + + // cy.getCookies() yields an array of cookies + cy.getCookies().should('have.length', 1).should((cookies) => { + + // each cookie has these properties + expect(cookies[0]).to.have.property('name', 'token') + expect(cookies[0]).to.have.property('value', '123ABC') + expect(cookies[0]).to.have.property('httpOnly', false) + expect(cookies[0]).to.have.property('secure', false) + expect(cookies[0]).to.have.property('domain') + expect(cookies[0]).to.have.property('path') + }) + }) + + it('cy.setCookie() - set a browser cookie', () => { + // https://on.cypress.io/setcookie + cy.getCookies().should('be.empty') + + cy.setCookie('foo', 'bar') + + // cy.getCookie() yields a cookie object + cy.getCookie('foo').should('have.property', 'value', 'bar') + }) + + it('cy.clearCookie() - clear a browser cookie', () => { + // https://on.cypress.io/clearcookie + cy.getCookie('token').should('be.null') + + cy.get('#clearCookie .set-a-cookie').click() + + cy.getCookie('token').should('have.property', 'value', '123ABC') + + // cy.clearCookies() yields null + cy.clearCookie('token').should('be.null') + + cy.getCookie('token').should('be.null') + }) + + it('cy.clearCookies() - clear browser cookies', () => { + // https://on.cypress.io/clearcookies + cy.getCookies().should('be.empty') + + cy.get('#clearCookies .set-a-cookie').click() + + cy.getCookies().should('have.length', 1) + + // cy.clearCookies() yields null + cy.clearCookies() + + cy.getCookies().should('be.empty') + }) +}) diff --git a/packages/example/cypress/integration/examples/cypress_api.spec.js b/packages/example/cypress/integration/examples/cypress_api.spec.js new file mode 100644 index 000000000000..eeaec5358301 --- /dev/null +++ b/packages/example/cypress/integration/examples/cypress_api.spec.js @@ -0,0 +1,211 @@ +/// <reference types="Cypress" /> + +context('Cypress.Commands', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + // https://on.cypress.io/custom-commands + + it('.add() - create a custom command', () => { + Cypress.Commands.add('console', { + prevSubject: true, + }, (subject, method) => { + // the previous subject is automatically received + // and the commands arguments are shifted + + // allow us to change the console method used + method = method || 'log' + + // log the subject to the console + console[method]('The subject is', subject) + + // whatever we return becomes the new subject + // we don't want to change the subject so + // we return whatever was passed in + return subject + }) + + cy.get('button').console('info').then(($button) => { + // subject is still $button + }) + }) +}) + + +context('Cypress.Cookies', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + // https://on.cypress.io/cookies + it('.debug() - enable or disable debugging', () => { + Cypress.Cookies.debug(true) + + // Cypress will now log in the console when + // cookies are set or cleared + cy.setCookie('fakeCookie', '123ABC') + cy.clearCookie('fakeCookie') + cy.setCookie('fakeCookie', '123ABC') + cy.clearCookie('fakeCookie') + cy.setCookie('fakeCookie', '123ABC') + }) + + it('.preserveOnce() - preserve cookies by key', () => { + // normally cookies are reset after each test + cy.getCookie('fakeCookie').should('not.be.ok') + + // preserving a cookie will not clear it when + // the next test starts + cy.setCookie('lastCookie', '789XYZ') + Cypress.Cookies.preserveOnce('lastCookie') + }) + + it('.defaults() - set defaults for all cookies', () => { + // now any cookie with the name 'session_id' will + // not be cleared before each new test runs + Cypress.Cookies.defaults({ + whitelist: 'session_id', + }) + }) +}) + + +context('Cypress.Server', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + // Permanently override server options for + // all instances of cy.server() + + // https://on.cypress.io/cypress-server + it('.defaults() - change default config of server', () => { + Cypress.Server.defaults({ + delay: 0, + force404: false, + whitelist (xhr) { + // handle custom logic for whitelisting + }, + }) + }) +}) + +context('Cypress.arch', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + it('Get CPU architecture name of underlying OS', () => { + // https://on.cypress.io/arch + expect(Cypress.arch).to.exist + }) +}) + +context('Cypress.config()', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + it('Get and set configuration options', () => { + // https://on.cypress.io/config + let myConfig = Cypress.config() + + expect(myConfig).to.have.property('animationDistanceThreshold', 5) + expect(myConfig).to.have.property('baseUrl', null) + expect(myConfig).to.have.property('defaultCommandTimeout', 4000) + expect(myConfig).to.have.property('requestTimeout', 5000) + expect(myConfig).to.have.property('responseTimeout', 30000) + expect(myConfig).to.have.property('viewportHeight', 660) + expect(myConfig).to.have.property('viewportWidth', 1000) + expect(myConfig).to.have.property('pageLoadTimeout', 60000) + expect(myConfig).to.have.property('waitForAnimations', true) + + expect(Cypress.config('pageLoadTimeout')).to.eq(60000) + + // this will change the config for the rest of your tests! + Cypress.config('pageLoadTimeout', 20000) + + expect(Cypress.config('pageLoadTimeout')).to.eq(20000) + + Cypress.config('pageLoadTimeout', 60000) + }) +}) + +context('Cypress.dom', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + // https://on.cypress.io/dom + it('.isHidden() - determine if a DOM element is hidden', () => { + let hiddenP = Cypress.$('.dom-p p.hidden').get(0) + let visibleP = Cypress.$('.dom-p p.visible').get(0) + + // our first paragraph has css class 'hidden' + expect(Cypress.dom.isHidden(hiddenP)).to.be.true + expect(Cypress.dom.isHidden(visibleP)).to.be.false + }) +}) + +context('Cypress.env()', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + // We can set environment variables for highly dynamic values + + // https://on.cypress.io/environment-variables + it('Get environment variables', () => { + // https://on.cypress.io/env + // set multiple environment variables + Cypress.env({ + host: 'veronica.dev.local', + api_server: 'http://localhost:8888/v1/', + }) + + // get environment variable + expect(Cypress.env('host')).to.eq('veronica.dev.local') + + // set environment variable + Cypress.env('api_server', 'http://localhost:8888/v2/') + expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') + + // get all environment variable + expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') + expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') + }) +}) + +context('Cypress.log', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + it('Control what is printed to the Command Log', () => { + // https://on.cypress.io/cypress-log + }) +}) + + +context('Cypress.platform', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + it('Get underlying OS name', () => { + // https://on.cypress.io/platform + expect(Cypress.platform).to.be.exist + }) +}) + +context('Cypress.version', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + it('Get current version of Cypress being run', () => { + // https://on.cypress.io/version + expect(Cypress.version).to.be.exist + }) +}) diff --git a/packages/example/cypress/integration/examples/files.spec.js b/packages/example/cypress/integration/examples/files.spec.js new file mode 100644 index 000000000000..46402a4ec4e2 --- /dev/null +++ b/packages/example/cypress/integration/examples/files.spec.js @@ -0,0 +1,86 @@ +/// <reference types="Cypress" /> + +context('Files', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/files') + }) + it('cy.fixture() - load a fixture', () => { + // https://on.cypress.io/fixture + + // Instead of writing a response inline you can + // use a fixture file's content. + + cy.server() + cy.fixture('example.json').as('comment') + cy.route('GET', 'comments/*', '@comment').as('getComment') + + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get('.fixture-btn').click() + + cy.wait('@getComment').its('responseBody') + .should('have.property', 'name') + .and('include', 'Using fixtures to represent data') + + // you can also just write the fixture in the route + cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment') + + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get('.fixture-btn').click() + + cy.wait('@getComment').its('responseBody') + .should('have.property', 'name') + .and('include', 'Using fixtures to represent data') + + // or write fx to represent fixture + // by default it assumes it's .json + cy.route('GET', 'comments/*', 'fx:example').as('getComment') + + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get('.fixture-btn').click() + + cy.wait('@getComment').its('responseBody') + .should('have.property', 'name') + .and('include', 'Using fixtures to represent data') + }) + + it('cy.readFile() - read a files contents', () => { + // https://on.cypress.io/readfile + + // You can read a file and yield its contents + // The filePath is relative to your project's root. + cy.readFile('cypress.json').then((json) => { + expect(json).to.be.an('object') + }) + }) + + it('cy.writeFile() - write to a file', () => { + // https://on.cypress.io/writefile + + // You can write to a file + + // Use a response from a request to automatically + // generate a fixture file for use later + cy.request('https://jsonplaceholder.typicode.com/users') + .then((response) => { + cy.writeFile('cypress/fixtures/users.json', response.body) + }) + cy.fixture('users').should((users) => { + expect(users[0].name).to.exist + }) + + // JavaScript arrays and objects are stringified + // and formatted into text. + cy.writeFile('cypress/fixtures/profile.json', { + id: 8739, + name: 'Jane', + email: 'jane@example.com', + }) + + cy.fixture('profile').should((profile) => { + expect(profile.name).to.eq('Jane') + }) + }) +}) diff --git a/packages/example/cypress/integration/examples/local_storage.spec.js b/packages/example/cypress/integration/examples/local_storage.spec.js new file mode 100644 index 000000000000..076b096fc373 --- /dev/null +++ b/packages/example/cypress/integration/examples/local_storage.spec.js @@ -0,0 +1,52 @@ +/// <reference types="Cypress" /> + +context('Local Storage', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/local-storage') + }) + // Although local storage is automatically cleared + // in between tests to maintain a clean state + // sometimes we need to clear the local storage manually + + it('cy.clearLocalStorage() - clear all data in local storage', () => { + // https://on.cypress.io/clearlocalstorage + cy.get('.ls-btn').click().should(() => { + expect(localStorage.getItem('prop1')).to.eq('red') + expect(localStorage.getItem('prop2')).to.eq('blue') + expect(localStorage.getItem('prop3')).to.eq('magenta') + }) + + // clearLocalStorage() yields the localStorage object + cy.clearLocalStorage().should((ls) => { + expect(ls.getItem('prop1')).to.be.null + expect(ls.getItem('prop2')).to.be.null + expect(ls.getItem('prop3')).to.be.null + }) + + // Clear key matching string in Local Storage + cy.get('.ls-btn').click().should(() => { + expect(localStorage.getItem('prop1')).to.eq('red') + expect(localStorage.getItem('prop2')).to.eq('blue') + expect(localStorage.getItem('prop3')).to.eq('magenta') + }) + + cy.clearLocalStorage('prop1').should((ls) => { + expect(ls.getItem('prop1')).to.be.null + expect(ls.getItem('prop2')).to.eq('blue') + expect(ls.getItem('prop3')).to.eq('magenta') + }) + + // Clear keys matching regex in Local Storage + cy.get('.ls-btn').click().should(() => { + expect(localStorage.getItem('prop1')).to.eq('red') + expect(localStorage.getItem('prop2')).to.eq('blue') + expect(localStorage.getItem('prop3')).to.eq('magenta') + }) + + cy.clearLocalStorage(/prop1|2/).should((ls) => { + expect(ls.getItem('prop1')).to.be.null + expect(ls.getItem('prop2')).to.be.null + expect(ls.getItem('prop3')).to.eq('magenta') + }) + }) +}) diff --git a/packages/example/cypress/integration/examples/location.spec.js b/packages/example/cypress/integration/examples/location.spec.js new file mode 100644 index 000000000000..68e755c101ca --- /dev/null +++ b/packages/example/cypress/integration/examples/location.spec.js @@ -0,0 +1,32 @@ +/// <reference types="Cypress" /> + +context('Location', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/location') + }) + + it('cy.hash() - get the current URL hash', () => { + // https://on.cypress.io/hash + cy.hash().should('be.empty') + }) + + it('cy.location() - get window.location', () => { + // https://on.cypress.io/location + cy.location().should((location) => { + expect(location.hash).to.be.empty + expect(location.href).to.eq('https://example.cypress.io/commands/location') + expect(location.host).to.eq('example.cypress.io') + expect(location.hostname).to.eq('example.cypress.io') + expect(location.origin).to.eq('https://example.cypress.io') + expect(location.pathname).to.eq('/commands/location') + expect(location.port).to.eq('') + expect(location.protocol).to.eq('https:') + expect(location.search).to.be.empty + }) + }) + + it('cy.url() - get the current URL', () => { + // https://on.cypress.io/url + cy.url().should('eq', 'https://example.cypress.io/commands/location') + }) +}) diff --git a/packages/example/cypress/integration/examples/misc.spec.js b/packages/example/cypress/integration/examples/misc.spec.js new file mode 100644 index 000000000000..db1f1381041e --- /dev/null +++ b/packages/example/cypress/integration/examples/misc.spec.js @@ -0,0 +1,68 @@ +/// <reference types="Cypress" /> + +context('Misc', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/misc') + }) + + it('.end() - end the command chain', () => { + // https://on.cypress.io/end + + // cy.end is useful when you want to end a chain of commands + // and force Cypress to re-query from the root element + cy.get('.misc-table').within(() => { + // ends the current chain and yields null + cy.contains('Cheryl').click().end() + + // queries the entire table again + cy.contains('Charles').click() + }) + }) + + it('cy.exec() - execute a system command', () => { + // https://on.cypress.io/exec + + // execute a system command. + // so you can take actions necessary for + // your test outside the scope of Cypress. + cy.exec('echo Jane Lane') + .its('stdout').should('contain', 'Jane Lane') + + // we can use Cypress.platform string to + // select appropriate command + // https://on.cypress/io/platform + cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) + + if (Cypress.platform === 'win32') { + cy.exec('print cypress.json') + .its('stderr').should('be.empty') + } else { + cy.exec('cat cypress.json') + .its('stderr').should('be.empty') + + cy.exec('pwd') + .its('code').should('eq', 0) + } + }) + + it('cy.focused() - get the DOM element that has focus', () => { + // https://on.cypress.io/focused + cy.get('.misc-form').find('#name').click() + cy.focused().should('have.id', 'name') + + cy.get('.misc-form').find('#description').click() + cy.focused().should('have.id', 'description') + }) + + it('cy.screenshot() - take a screenshot', () => { + // https://on.cypress.io/screenshot + cy.screenshot('my-image') + }) + + it('cy.wrap() - wrap an object', () => { + // https://on.cypress.io/wrap + cy.wrap({ foo: 'bar' }) + .should('have.property', 'foo') + .and('include', 'bar') + }) +}) diff --git a/packages/example/cypress/integration/examples/navigation.spec.js b/packages/example/cypress/integration/examples/navigation.spec.js new file mode 100644 index 000000000000..04bd0ce153ac --- /dev/null +++ b/packages/example/cypress/integration/examples/navigation.spec.js @@ -0,0 +1,54 @@ +/// <reference types="Cypress" /> + +context('Navigation', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io') + cy.get('.navbar-nav').contains('Commands').click() + cy.get('.dropdown-menu').contains('Navigation').click() + }) + + it('cy.go() - go back or forward in the browser\'s history', () => { + // https://on.cypress.io/go + + cy.location('pathname').should('include', 'navigation') + + cy.go('back') + cy.location('pathname').should('not.include', 'navigation') + + cy.go('forward') + cy.location('pathname').should('include', 'navigation') + + // clicking back + cy.go(-1) + cy.location('pathname').should('not.include', 'navigation') + + // clicking forward + cy.go(1) + cy.location('pathname').should('include', 'navigation') + }) + + it('cy.reload() - reload the page', () => { + // https://on.cypress.io/reload + cy.reload() + + // reload the page without using the cache + cy.reload(true) + }) + + it('cy.visit() - visit a remote url', () => { + // https://on.cypress.io/visit + + // Visit any sub-domain of your current domain + + // Pass options to the visit + cy.visit('https://example.cypress.io/commands/navigation', { + timeout: 50000, // increase total time for the visit to resolve + onBeforeLoad (contentWindow) { + // contentWindow is the remote page's window object + }, + onLoad (contentWindow) { + // contentWindow is the remote page's window object + }, + }) + }) +}) diff --git a/packages/example/cypress/integration/examples/network_requests.spec.js b/packages/example/cypress/integration/examples/network_requests.spec.js new file mode 100644 index 000000000000..3c67d44cdfc5 --- /dev/null +++ b/packages/example/cypress/integration/examples/network_requests.spec.js @@ -0,0 +1,108 @@ +/// <reference types="Cypress" /> + +context('Network Requests', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/network-requests') + }) + + // Manage AJAX / XHR requests in your app + + it('cy.server() - control behavior of network requests and responses', () => { + // https://on.cypress.io/server + + cy.server().should((server) => { + // the default options on server + // you can override any of these options + expect(server.delay).to.eq(0) + expect(server.method).to.eq('GET') + expect(server.status).to.eq(200) + expect(server.headers).to.be.null + expect(server.response).to.be.null + expect(server.onRequest).to.be.undefined + expect(server.onResponse).to.be.undefined + expect(server.onAbort).to.be.undefined + + // These options control the server behavior + // affecting all requests + + // pass false to disable existing route stubs + expect(server.enable).to.be.true + // forces requests that don't match your routes to 404 + expect(server.force404).to.be.false + // whitelists requests from ever being logged or stubbed + expect(server.whitelist).to.be.a('function') + }) + + cy.server({ + method: 'POST', + delay: 1000, + status: 422, + response: {}, + }) + + // any route commands will now inherit the above options + // from the server. anything we pass specifically + // to route will override the defaults though. + }) + + it('cy.request() - make an XHR request', () => { + // https://on.cypress.io/request + cy.request('https://jsonplaceholder.typicode.com/comments') + .should((response) => { + expect(response.status).to.eq(200) + expect(response.body).to.have.length(500) + expect(response).to.have.property('headers') + expect(response).to.have.property('duration') + }) + }) + + it('cy.route() - route responses to matching requests', () => { + // https://on.cypress.io/route + + let message = 'whoa, this comment does not exist' + cy.server() + + // Listen to GET to comments/1 + cy.route('GET', 'comments/*').as('getComment') + + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get('.network-btn').click() + + // https://on.cypress.io/wait + cy.wait('@getComment').its('status').should('eq', 200) + + // Listen to POST to comments + cy.route('POST', '/comments').as('postComment') + + // we have code that posts a comment when + // the button is clicked in scripts.js + cy.get('.network-post').click() + cy.wait('@postComment') + + // get the route + cy.get('@postComment').should((xhr) => { + expect(xhr.requestBody).to.include('email') + expect(xhr.requestHeaders).to.have.property('Content-Type') + expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()') + }) + + // Stub a response to PUT comments/ **** + cy.route({ + method: 'PUT', + url: 'comments/*', + status: 404, + response: { error: message }, + delay: 500, + }).as('putComment') + + // we have code that puts a comment when + // the button is clicked in scripts.js + cy.get('.network-put').click() + + cy.wait('@putComment') + + // our 404 statusCode logic in scripts.js executed + cy.get('.network-put-comment').should('contain', message) + }) +}) diff --git a/packages/example/cypress/integration/examples/querying.spec.js b/packages/example/cypress/integration/examples/querying.spec.js new file mode 100644 index 000000000000..7cb34adce7f1 --- /dev/null +++ b/packages/example/cypress/integration/examples/querying.spec.js @@ -0,0 +1,65 @@ +/// <reference types="Cypress" /> + +context('Querying', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/querying') + }) + + // The most commonly used query is 'cy.get()', you can + // think of this like the '$' in jQuery + + it('cy.get() - query DOM elements', () => { + // https://on.cypress.io/get + + cy.get('#query-btn').should('contain', 'Button') + + cy.get('.query-btn').should('contain', 'Button') + + cy.get('#querying .well>button:first').should('contain', 'Button') + // ↲ + // Use CSS selectors just like jQuery + }) + + it('cy.contains() - query DOM elements with matching content', () => { + // https://on.cypress.io/contains + cy.get('.query-list') + .contains('bananas').should('have.class', 'third') + + // we can pass a regexp to `.contains()` + cy.get('.query-list') + .contains(/^b\w+/).should('have.class', 'third') + + cy.get('.query-list') + .contains('apples').should('have.class', 'first') + + // passing a selector to contains will + // yield the selector containing the text + cy.get('#querying') + .contains('ul', 'oranges') + .should('have.class', 'query-list') + + cy.get('.query-button') + .contains('Save Form') + .should('have.class', 'btn') + }) + + it('.within() - query DOM elements within a specific element', () => { + // https://on.cypress.io/within + cy.get('.query-form').within(() => { + cy.get('input:first').should('have.attr', 'placeholder', 'Email') + cy.get('input:last').should('have.attr', 'placeholder', 'Password') + }) + }) + + it('cy.root() - query the root DOM element', () => { + // https://on.cypress.io/root + + // By default, root is the document + cy.root().should('match', 'html') + + cy.get('.query-ul').within(() => { + // In this within, the root is now the ul DOM element + cy.root().should('have.class', 'query-ul') + }) + }) +}) diff --git a/packages/example/cypress/integration/examples/spies_stubs_clocks.spec.js b/packages/example/cypress/integration/examples/spies_stubs_clocks.spec.js new file mode 100644 index 000000000000..448e8a8d0507 --- /dev/null +++ b/packages/example/cypress/integration/examples/spies_stubs_clocks.spec.js @@ -0,0 +1,62 @@ +/// <reference types="Cypress" /> + +context('Spies, Stubs, and Clock', () => { + it('cy.spy() - wrap a method in a spy', () => { + // https://on.cypress.io/spy + cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') + + let obj = { + foo () {}, + } + + let spy = cy.spy(obj, 'foo').as('anyArgs') + + obj.foo() + + expect(spy).to.be.called + }) + + it('cy.stub() - create a stub and/or replace a function with stub', () => { + // https://on.cypress.io/stub + cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') + + let obj = { + foo () {}, + } + + let stub = cy.stub(obj, 'foo').as('foo') + + obj.foo('foo', 'bar') + + expect(stub).to.be.called + }) + + it('cy.clock() - control time in the browser', () => { + // https://on.cypress.io/clock + + // create the date in UTC so its always the same + // no matter what local timezone the browser is running in + let now = new Date(Date.UTC(2017, 2, 14)).getTime() + + cy.clock(now) + cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') + cy.get('#clock-div').click() + .should('have.text', '1489449600') + }) + + it('cy.tick() - move time in the browser', () => { + // https://on.cypress.io/tick + + // create the date in UTC so its always the same + // no matter what local timezone the browser is running in + let now = new Date(Date.UTC(2017, 2, 14)).getTime() + + cy.clock(now) + cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') + cy.get('#tick-div').click() + .should('have.text', '1489449600') + cy.tick(10000) // 10 seconds passed + cy.get('#tick-div').click() + .should('have.text', '1489449610') + }) +}) diff --git a/packages/example/cypress/integration/examples/traversal.spec.js b/packages/example/cypress/integration/examples/traversal.spec.js new file mode 100644 index 000000000000..1082eca6cc13 --- /dev/null +++ b/packages/example/cypress/integration/examples/traversal.spec.js @@ -0,0 +1,121 @@ +/// <reference types="Cypress" /> + +context('Traversal', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/traversal') + }) + + it('.children() - get child DOM elements', () => { + // https://on.cypress.io/children + cy.get('.traversal-breadcrumb') + .children('.active') + .should('contain', 'Data') + }) + + it('.closest() - get closest ancestor DOM element', () => { + // https://on.cypress.io/closest + cy.get('.traversal-badge') + .closest('ul') + .should('have.class', 'list-group') + }) + + it('.eq() - get a DOM element at a specific index', () => { + // https://on.cypress.io/eq + cy.get('.traversal-list>li') + .eq(1).should('contain', 'siamese') + }) + + it('.filter() - get DOM elements that match the selector', () => { + // https://on.cypress.io/filter + cy.get('.traversal-nav>li') + .filter('.active').should('contain', 'About') + }) + + it('.find() - get descendant DOM elements of the selector', () => { + // https://on.cypress.io/find + cy.get('.traversal-pagination') + .find('li').find('a') + .should('have.length', 7) + }) + + it('.first() - get first DOM element', () => { + // https://on.cypress.io/first + cy.get('.traversal-table td') + .first().should('contain', '1') + }) + + it('.last() - get last DOM element', () => { + // https://on.cypress.io/last + cy.get('.traversal-buttons .btn') + .last().should('contain', 'Submit') + }) + + it('.next() - get next sibling DOM element', () => { + // https://on.cypress.io/next + cy.get('.traversal-ul') + .contains('apples').next().should('contain', 'oranges') + }) + + it('.nextAll() - get all next sibling DOM elements', () => { + // https://on.cypress.io/nextall + cy.get('.traversal-next-all') + .contains('oranges') + .nextAll().should('have.length', 3) + }) + + it('.nextUntil() - get next sibling DOM elements until next el', () => { + // https://on.cypress.io/nextuntil + cy.get('#veggies') + .nextUntil('#nuts').should('have.length', 3) + }) + + it('.not() - remove DOM elements from set of DOM elements', () => { + // https://on.cypress.io/not + cy.get('.traversal-disabled .btn') + .not('[disabled]').should('not.contain', 'Disabled') + }) + + it('.parent() - get parent DOM element from DOM elements', () => { + // https://on.cypress.io/parent + cy.get('.traversal-mark') + .parent().should('contain', 'Morbi leo risus') + }) + + it('.parents() - get parent DOM elements from DOM elements', () => { + // https://on.cypress.io/parents + cy.get('.traversal-cite') + .parents().should('match', 'blockquote') + }) + + it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { + // https://on.cypress.io/parentsuntil + cy.get('.clothes-nav') + .find('.active') + .parentsUntil('.clothes-nav') + .should('have.length', 2) + }) + + it('.prev() - get previous sibling DOM element', () => { + // https://on.cypress.io/prev + cy.get('.birds').find('.active') + .prev().should('contain', 'Lorikeets') + }) + + it('.prevAll() - get all previous sibling DOM elements', () => { + // https://on.cypress.io/prevAll + cy.get('.fruits-list').find('.third') + .prevAll().should('have.length', 2) + }) + + it('.prevUntil() - get all previous sibling DOM elements until el', () => { + // https://on.cypress.io/prevUntil + cy.get('.foods-list').find('#nuts') + .prevUntil('#veggies').should('have.length', 3) + }) + + it('.siblings() - get all sibling DOM elements', () => { + // https://on.cypress.io/siblings + cy.get('.traversal-pills .active') + .siblings().should('have.length', 2) + }) +}) diff --git a/packages/example/cypress/integration/examples/utilities.spec.js b/packages/example/cypress/integration/examples/utilities.spec.js new file mode 100644 index 000000000000..2356c94645d1 --- /dev/null +++ b/packages/example/cypress/integration/examples/utilities.spec.js @@ -0,0 +1,89 @@ +/// <reference types="Cypress" /> + +context('Utilities', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/utilities') + }) + + it('Cypress._ - call a lodash method', () => { + // https://on.cypress.io/_ + cy.request('https://jsonplaceholder.typicode.com/users') + .then((response) => { + let ids = Cypress._.chain(response.body).map('id').take(3).value() + + expect(ids).to.deep.eq([1, 2, 3]) + }) + }) + + it('Cypress.$ - call a jQuery method', () => { + // https://on.cypress.io/$ + let $li = Cypress.$('.utility-jquery li:first') + + cy.wrap($li) + .should('not.have.class', 'active') + .click() + .should('have.class', 'active') + }) + + it('Cypress.Blob - blob utilities and base64 string conversion', () => { + // https://on.cypress.io/blob + cy.get('.utility-blob').then(($div) => + // https://github.com/nolanlawson/blob-util#imgSrcToDataURL + // get the dataUrl string for the javascript-logo + Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') + .then((dataUrl) => { + // create an <img> element and set its src to the dataUrl + let img = Cypress.$('<img />', { src: dataUrl }) + // need to explicitly return cy here since we are initially returning + // the Cypress.Blob.imgSrcToDataURL promise to our test + // append the image + $div.append(img) + + cy.get('.utility-blob img').click() + .should('have.attr', 'src', dataUrl) + })) + }) + + it('Cypress.minimatch - test out glob patterns against strings', () => { + // https://on.cypress.io/minimatch + Cypress.minimatch('/users/1/comments', '/users/*/comments', { + matchBase: true, + }) + }) + + + it('Cypress.moment() - format or parse dates using a moment method', () => { + // https://on.cypress.io/moment + let time = Cypress.moment().utc('2014-04-25T19:38:53.196Z').format('h:mm A') + + cy.get('.utility-moment').contains('3:38 PM') + .should('have.class', 'badge') + }) + + + it('Cypress.Promise - instantiate a bluebird promise', () => { + // https://on.cypress.io/promise + let waited = false + + function waitOneSecond () { + // return a promise that resolves after 1 second + return new Cypress.Promise((resolve, reject) => { + setTimeout(() => { + // set waited to true + waited = true + + // resolve with 'foo' string + resolve('foo') + }, 1000) + }) + } + + cy.then(() => + // return a promise to cy.then() that + // is awaited until it resolves + waitOneSecond().then((str) => { + expect(str).to.eq('foo') + expect(waited).to.be.true + })) + }) +}) diff --git a/packages/example/cypress/integration/examples/viewport.spec.js b/packages/example/cypress/integration/examples/viewport.spec.js new file mode 100644 index 000000000000..711fe74efb11 --- /dev/null +++ b/packages/example/cypress/integration/examples/viewport.spec.js @@ -0,0 +1,59 @@ +/// <reference types="Cypress" /> + +context('Viewport', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/viewport') + }) + + it('cy.viewport() - set the viewport size and dimension', () => { + // https://on.cypress.io/viewport + + cy.get('#navbar').should('be.visible') + cy.viewport(320, 480) + + // the navbar should have collapse since our screen is smaller + cy.get('#navbar').should('not.be.visible') + cy.get('.navbar-toggle').should('be.visible').click() + cy.get('.nav').find('a').should('be.visible') + + // lets see what our app looks like on a super large screen + cy.viewport(2999, 2999) + + // cy.viewport() accepts a set of preset sizes + // to easily set the screen to a device's width and height + + // We added a cy.wait() between each viewport change so you can see + // the change otherwise it is a little too fast to see :) + + cy.viewport('macbook-15') + cy.wait(200) + cy.viewport('macbook-13') + cy.wait(200) + cy.viewport('macbook-11') + cy.wait(200) + cy.viewport('ipad-2') + cy.wait(200) + cy.viewport('ipad-mini') + cy.wait(200) + cy.viewport('iphone-6+') + cy.wait(200) + cy.viewport('iphone-6') + cy.wait(200) + cy.viewport('iphone-5') + cy.wait(200) + cy.viewport('iphone-4') + cy.wait(200) + cy.viewport('iphone-3') + cy.wait(200) + + // cy.viewport() accepts an orientation for all presets + // the default orientation is 'portrait' + cy.viewport('ipad-2', 'portrait') + cy.wait(200) + cy.viewport('iphone-4', 'landscape') + cy.wait(200) + + // The viewport will be reset back to the default dimensions + // in between tests (the default can be set in cypress.json) + }) +}) diff --git a/packages/example/cypress/integration/examples/waiting.spec.js b/packages/example/cypress/integration/examples/waiting.spec.js new file mode 100644 index 000000000000..e11d9ca9382f --- /dev/null +++ b/packages/example/cypress/integration/examples/waiting.spec.js @@ -0,0 +1,34 @@ +/// <reference types="Cypress" /> + +context('Waiting', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/waiting') + }) + // BE CAREFUL of adding unnecessary wait times. + // https://on.cypress.io/best-practices#Unnecessary-Waiting + + // https://on.cypress.io/wait + it('cy.wait() - wait for a specific amount of time', () => { + cy.get('.wait-input1').type('Wait 1000ms after typing') + cy.wait(1000) + cy.get('.wait-input2').type('Wait 1000ms after typing') + cy.wait(1000) + cy.get('.wait-input3').type('Wait 1000ms after typing') + cy.wait(1000) + }) + + it('cy.wait() - wait for a specific route', () => { + cy.server() + + // Listen to GET to comments/1 + cy.route('GET', 'comments/*').as('getComment') + + // we have code that gets a comment when + // the button is clicked in scripts.js + cy.get('.network-btn').click() + + // wait for GET comments/1 + cy.wait('@getComment').its('status').should('eq', 200) + }) + +}) diff --git a/packages/example/cypress/integration/examples/window.spec.js b/packages/example/cypress/integration/examples/window.spec.js new file mode 100644 index 000000000000..00bff9f7fa99 --- /dev/null +++ b/packages/example/cypress/integration/examples/window.spec.js @@ -0,0 +1,22 @@ +/// <reference types="Cypress" /> + +context('Window', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/window') + }) + + it('cy.window() - get the global window object', () => { + // https://on.cypress.io/window + cy.window().should('have.property', 'top') + }) + + it('cy.document() - get the document object', () => { + // https://on.cypress.io/document + cy.document().should('have.property', 'charset').and('eq', 'UTF-8') + }) + + it('cy.title() - get the title', () => { + // https://on.cypress.io/title + cy.title().should('include', 'Kitchen Sink') + }) +}) diff --git a/packages/example/lib/example.js b/packages/example/lib/example.js index bd8b06295c2c..924427353137 100644 --- a/packages/example/lib/example.js +++ b/packages/example/lib/example.js @@ -1,7 +1,12 @@ +const glob = require('glob') const path = require('path') module.exports = { - getPathToExample () { - return path.join(__dirname, '..', 'cypress', 'integration', 'example_spec.js') + getPathToExamples () { + return glob.sync(path.join(__dirname, '..', 'cypress', 'integration', 'examples', '**', '*')) + }, + + getFolderName () { + return 'examples' }, } diff --git a/packages/example/package.json b/packages/example/package.json index d2f6d51a1780..83c1dfb04d3e 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -22,7 +22,7 @@ "bin-up": "^1.1.0", "chai": "^3.5.0", "cross-env": "^5.0.5", - "cypress-example-kitchensink": "0.8.3", + "cypress-example-kitchensink": "1.0.1", "glob": "^7.0.3", "gulp": "^3.9.1", "gulp-clean": "^0.3.1", diff --git a/packages/reporter/jsconfig.json b/packages/reporter/jsconfig.json new file mode 100644 index 000000000000..504cd646e149 --- /dev/null +++ b/packages/reporter/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + } +} diff --git a/packages/reporter/src/commands/command-model.js b/packages/reporter/src/commands/command-model.js index 1a29de91e719..d5a5709f8098 100644 --- a/packages/reporter/src/commands/command-model.js +++ b/packages/reporter/src/commands/command-model.js @@ -1,4 +1,4 @@ -import { action, observable } from 'mobx' +import { action, computed, observable } from 'mobx' import Err from '../lib/err-model' import Instrument from '../instruments/instrument-model' @@ -13,9 +13,24 @@ export default class Command extends Instrument { @observable number @observable numElements @observable visible = true + @observable duplicates = [] + @observable isDuplicate = false _prevState = null + @computed get displayMessage () { + return this.renderProps.message || this.message + } + + @computed get numDuplicates () { + // and one to include self so it's the total number of same events + return this.duplicates.length + 1 + } + + @computed get hasDuplicates () { + return this.numDuplicates > 1 + } + constructor (props) { super(props) @@ -41,6 +56,23 @@ export default class Command extends Instrument { this._checkLongRunning() } + isMatchingEvent (command) { + return command.event && this.matches(command) + } + + addDuplicate (command) { + command.isDuplicate = true + this.duplicates.push(command) + } + + matches (command) { + return ( + command.type === this.type && + command.name === this.name && + command.displayMessage === this.displayMessage + ) + } + // the following several methods track if the command's state has been // active for more than the LONG_RUNNING_THRESHOLD and set the // isLongRunning flag to true, which propagates up to the test to diff --git a/packages/reporter/src/commands/command.jsx b/packages/reporter/src/commands/command.jsx index 038043348281..d3b635c52704 100644 --- a/packages/reporter/src/commands/command.jsx +++ b/packages/reporter/src/commands/command.jsx @@ -1,7 +1,7 @@ import _ from 'lodash' import cs from 'classnames' import Markdown from 'markdown-it' -import { action } from 'mobx' +import { action, observable } from 'mobx' import { observer } from 'mobx-react' import React, { Component } from 'react' import Tooltip from '@cypress/react-tooltip' @@ -15,7 +15,6 @@ const md = new Markdown() const displayName = (model) => model.displayName || model.name const nameClassName = (name) => name.replace(/(\s+)/g, '-') -const getMessage = (model) => model.renderProps.message || model.message const formattedMessage = (message) => message ? md.renderInline(message) : '' const visibleMessage = (model) => { if (model.visible) return '' @@ -41,7 +40,7 @@ const Aliases = observer(({ model }) => { return ( <span> {_.map([].concat(model.alias), (alias) => ( - <Tooltip key={alias} placement='top' title={`${model.message} aliased as: '${alias}'`}> + <Tooltip key={alias} placement='top' title={`${model.displayMessage} aliased as: '${alias}'`}> <span className={`command-alias ${model.aliasType}`}>{alias}</span> </Tooltip> ))} @@ -52,13 +51,17 @@ const Aliases = observer(({ model }) => { const Message = observer(({ model }) => ( <span> <i className={`fa fa-circle ${model.renderProps.indicator}`}></i> - <span className='command-message-text' dangerouslySetInnerHTML={{ __html: formattedMessage(getMessage(model) - ) }} /> + <span + className='command-message-text' + dangerouslySetInnerHTML={{ __html: formattedMessage(model.displayMessage) }} + /> </span> )) @observer class Command extends Component { + @observable isOpen = false + static defaultProps = { appState, events, @@ -67,7 +70,7 @@ class Command extends Component { render () { const { model } = this.props - const message = getMessage(model) + const message = model.displayMessage return ( <li @@ -86,6 +89,9 @@ class Command extends Component { 'command-scaled': message && message.length > 100, 'no-elements': !model.numElements, 'multiple-elements': model.numElements > 1, + 'command-has-duplicates': model.hasDuplicates, + 'command-is-duplicate': model.isDuplicate, + 'command-is-open': this.isOpen, } )} onMouseOver={() => this._snapshot(true)} @@ -104,6 +110,9 @@ class Command extends Component { <span className='command-pin'> <i className='fa fa-thumb-tack'></i> </span> + <span className='command-expander' onClick={this._toggleOpen}> + <i className='fa'></i> + </span> <span className='command-method'> <span>{model.event ? `(${displayName(model)})` : displayName(model)}</span> </span> @@ -118,13 +127,37 @@ class Command extends Component { <Tooltip placement='top' title={`${model.numElements} matched elements`}> <span className='num-elements'>{model.numElements}</span> </Tooltip> + <Tooltip placement='top' title={`This event occurred ${model.numDuplicates} times`}> + <span className='num-duplicates'>{model.numDuplicates}</span> + </Tooltip> </span> </div> </FlashOnClick> + {this._duplicates()} </li> ) } + _duplicates () { + const { appState, events, model, runnablesStore } = this.props + + if (!this.isOpen || !model.hasDuplicates) return null + + return ( + <ul className='duplicates'> + {_.map(model.duplicates, (duplicate) => ( + <Command + key={duplicate.id} + model={duplicate} + appState={appState} + events={events} + runnablesStore={runnablesStore} + /> + ))} + </ul> + ) + } + _isPinned () { return this.props.appState.pinnedSnapshotId === this.props.model.id } @@ -138,6 +171,12 @@ class Command extends Component { return !this.props.appState.isRunning && this._isPinned() } + @action _toggleOpen = (e) => { + e.stopPropagation() + + this.isOpen = !this.isOpen + } + @action _onClick = () => { if (this.props.appState.isRunning) return @@ -172,14 +211,14 @@ class Command extends Component { // 50ms, it won't show the snapshot at all. so we // optimize for both snapshot showing + restoring _snapshot (show) { - const { runnablesStore } = this.props + const { model, runnablesStore } = this.props if (show) { runnablesStore.attemptingShowSnapshot = true this._showTimeout = setTimeout(() => { runnablesStore.showingSnapshot = true - this.props.events.emit('show:snapshot', this.props.model.id) + this.props.events.emit('show:snapshot', model.id) }, 50) } else { runnablesStore.attemptingShowSnapshot = false @@ -190,7 +229,7 @@ class Command extends Component { // we aren't trying to show a different snapshot if (runnablesStore.showingSnapshot && !runnablesStore.attemptingShowSnapshot) { runnablesStore.showingSnapshot = false - this.props.events.emit('hide:snapshot', this.props.model.id) + this.props.events.emit('hide:snapshot', model.id) } }, 50) } diff --git a/packages/reporter/src/commands/command.spec.jsx b/packages/reporter/src/commands/command.spec.jsx index 0948973f3909..5ac8ba36f2a5 100644 --- a/packages/reporter/src/commands/command.spec.jsx +++ b/packages/reporter/src/commands/command.spec.jsx @@ -13,13 +13,15 @@ const model = (props) => { return _.extend({ event: false, id: 'c1', - message: 'The message', + displayMessage: 'The message', name: 'The name', number: 1, numElements: 0, renderProps: {}, state: 'passed', type: 'parent', + hasDuplicates: false, + duplicates: [], }, props) } @@ -102,7 +104,7 @@ describe('<Command />', () => { }) it('renders with the scaled class when it has a renderProps message over 100 chars long', () => { - const component = shallow(<Command model={model({ renderProps: { message: longText } })} />) + const component = shallow(<Command model={model({ displayMessage: longText })} />) expect(component).to.have.className('command-scaled') }) @@ -130,28 +132,18 @@ describe('<Command />', () => { }) context('message', () => { - it('renders the message', () => { + it('renders the displayMessage', () => { const component = shallow(<Command model={model()} />) expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain('The message') }) - it('renders the renderProps message when specified', () => { - const component = shallow(<Command model={model({ renderProps: { message: 'The display message' } })} />) - expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain('The display message') - }) - - it('does not truncate the renderProps message when over 100 chars', () => { - const component = shallow(<Command model={model({ renderProps: { message: longText } })} />) + it('does not truncate the message when over 100 chars', () => { + const component = shallow(<Command model={model({ displayMessage: longText })} />) expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain(longText) }) - it('renders markdown in message', () => { - const component = shallow(<Command model={model({ message: withMarkdown })} />) - expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain(fromMarkdown) - }) - - it('renders markdown in renderProps message', () => { - const component = shallow(<Command model={model({ renderProps: { message: withMarkdown } })} />) + it('renders markdown', () => { + const component = shallow(<Command model={model({ displayMessage: withMarkdown })} />) expect(component.find(Message).first().shallow().find('.command-message-text').html()).to.contain(fromMarkdown) }) @@ -438,4 +430,39 @@ describe('<Command />', () => { }) }) }) + + context('duplicates', () => { + const withDuplicates = { hasDuplicates: true, numDuplicates: 5 } + + it('renders with command-has-duplicates class if it has duplicates', () => { + const component = shallow(<Command model={model(withDuplicates)} />) + expect(component).to.have.className('command-has-duplicates') + }) + + it('renders without command-has-duplicates class if no duplicates', () => { + const component = shallow(<Command model={model()} />) + expect(component).not.to.have.className('command-has-duplicates') + }) + + it('renders with command-is-duplicate class if it is a duplicate', () => { + const component = shallow(<Command model={model({ isDuplicate: true })} />) + expect(component).to.have.className('command-is-duplicate') + }) + + it('renders without command-is-duplicate class if it is not a duplicate', () => { + const component = shallow(<Command model={model()} />) + expect(component).not.to.have.className('command-is-duplicate') + }) + + it('displays number of duplicates', () => { + const component = shallow(<Command model={model({ hasDuplicates: true, numDuplicates: 5 })} />) + expect(component.find('.num-duplicates')).to.have.text('5') + }) + + it('opens after clicking expander', () => { + const component = shallow(<Command model={model(withDuplicates)} />) + component.find('.command-expander').simulate('click', { stopPropagation: () => {} }) + expect(component).to.have.className('command-is-open') + }) + }) }) diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index e0acbc95e122..25fd37a27541 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -53,26 +53,7 @@ .command { cursor: pointer; - padding: 1px 5px; margin: 0; - - &:hover { - background-color: #E0E5E7; - box-shadow: 0 1px 2px #ccc inset; - } - - &:hover .command-controls i.fa-pause { - visibility: visible; - } - - &.active { - background-color: #E1E1E1 !important; - box-shadow: 0 1px 6px #ccc inset; - } - - &.highlight { - background-color: #FFB61C !important; - } } .command-scaled { @@ -83,10 +64,10 @@ .command-is-event { font-style: italic; - span { + .command-method, + .command-message { color: #9a9aaa !important; } - } .command-type-parent { @@ -116,6 +97,12 @@ display: flex; flex-wrap: wrap; color: #777; + padding: 1px 5px; + + &:hover { + background-color: #E0E5E7; + box-shadow: 0 1px 2px #ccc inset; + } .command-alias { border-radius: 10px; @@ -355,15 +342,18 @@ display: inline-block; } - .command-has-num-elements .num-elements { + .command-has-num-elements .num-elements, + .num-duplicates { display: none; } .command-has-num-elements.no-elements .num-elements, - .command-has-num-elements.multiple-elements .num-elements { + .command-has-num-elements.multiple-elements .num-elements, + .command-has-duplicates .num-duplicates { display: inline; } + .command-is-duplicate .num-duplicates, .command-name-assert.command-has-num-elements .num-elements { display: none; } @@ -385,6 +375,59 @@ } } + .command-expander { + color: #bcbccc; + display: none; + text-align: right; + padding-right: 8px; + width: 25px; + + i { + @extend .#{$fa-css-prefix}-caret-right; + } + + &:hover { + color: #999; + } + } + + .command-has-duplicates, + .command-has-duplicates:hover { + .command-number { + display: block; + } + + .command-number { + display: none; + } + } + + .command-has-duplicates .command-expander { + display: block; + } + + .command-is-duplicate { + &:first-child { + border-top: solid 1px #e3e3e3; + } + + .command-expander { + visibility: hidden; + } + } + + .command-is-open { + .command-expander { + i { + @extend .#{$fa-css-prefix}-caret-down; + } + } + + .num-duplicates { + display: none; + } + } + .command-is-pinned, .command:hover { .command-number { @@ -396,7 +439,24 @@ } } - .command-is-pinned { + .command-has-duplicates:hover .duplicates .command-pin, + .command-has-duplicates:hover > span > .command-wrapper .command-pin { + display: none; + } + + .command-has-duplicates.command-is-pinned > span > .command-wrapper, + .command-is-duplicate.command-is-pinned > span > .command-wrapper, + .command-is-duplicate > span > .command-wrapper:hover { + .command-expander { + display: none; + } + + .command-pin { + display: block; + } + } + + .command-is-pinned > span > .command-wrapper { background: lighten($pinned, 40%); border-left: 2px solid $pinned; padding-left: 3px; @@ -415,17 +475,6 @@ } } - &.is-running .command, - .command-other-pinned { - .command-number { - display: block !important; - } - - .command-pin { - display: none !important; - } - } - .no-commands { background-color: #f5f5f5; border: 1px solid #e3e3e3; diff --git a/packages/reporter/src/header/controls.jsx b/packages/reporter/src/header/controls.jsx index 514dc4fed975..7392fd33d149 100644 --- a/packages/reporter/src/header/controls.jsx +++ b/packages/reporter/src/header/controls.jsx @@ -50,7 +50,7 @@ const Controls = observer(({ events, appState }) => { </Tooltip> ))} {ifThen(!appState.isRunning, ( - <Tooltip placement='bottom' title='Run All Tests'> + <Tooltip placement='bottom' title='Run all tests'> <button className='restart' onClick={emit('restart')}> <i className='fa fa-repeat'></i> </button> diff --git a/packages/reporter/src/header/controls.spec.jsx b/packages/reporter/src/header/controls.spec.jsx index 50ea6624c5e8..e72e80500293 100644 --- a/packages/reporter/src/header/controls.spec.jsx +++ b/packages/reporter/src/header/controls.spec.jsx @@ -160,7 +160,7 @@ describe('<Controls />', () => { it('renders tooltip around restart button', () => { const component = shallow(<Controls events={eventsStub()} appState={appState} />) - expect(component.find('.restart').parent()).to.have.prop('title', 'Run All Tests') + expect(component.find('.restart').parent()).to.have.prop('title', 'Run all tests') }) it('emits restart event when restart button is clicked', () => { diff --git a/packages/reporter/src/hooks/hook-model.js b/packages/reporter/src/hooks/hook-model.js index d9a925f439e3..94bf505e03e9 100644 --- a/packages/reporter/src/hooks/hook-model.js +++ b/packages/reporter/src/hooks/hook-model.js @@ -18,12 +18,17 @@ export default class Hook { command.number = this._currentNumber this._currentNumber++ } - this.commands.push(command) + const lastCommand = _.last(this.commands) + if (lastCommand && lastCommand.isMatchingEvent(command)) { + lastCommand.addDuplicate(command) + } else { + this.commands.push(command) + } } commandMatchingErr (errToMatch) { return _(this.commands) - .filter(({ err }) => err.displayMessage === errToMatch.displayMessage) - .last() + .filter(({ err }) => err.displayMessage === errToMatch.displayMessage) + .last() } } diff --git a/packages/reporter/src/hooks/hook-model.spec.js b/packages/reporter/src/hooks/hook-model.spec.js index 56b43eeab87c..879fb7b2003f 100644 --- a/packages/reporter/src/hooks/hook-model.spec.js +++ b/packages/reporter/src/hooks/hook-model.spec.js @@ -1,3 +1,4 @@ +import sinon from 'sinon' import Hook from './hook-model' describe('Hook model', () => { @@ -13,14 +14,14 @@ describe('Hook model', () => { context('#addCommand', () => { it('adds the command to its command collection', () => { - hook.addCommand({}) + hook.addCommand({ isMatchingEvent: () => false }) expect(hook.commands.length).to.equal(1) hook.addCommand({}) expect(hook.commands.length).to.equal(2) }) it('numbers commands incrementally when not events', () => { - const command1 = { event: false } + const command1 = { event: false, isMatchingEvent: () => false } hook.addCommand(command1) expect(command1.number).to.equal(1) @@ -30,11 +31,11 @@ describe('Hook model', () => { }) it('does not number event commands', () => { - const command1 = { event: false } + const command1 = { event: false, isMatchingEvent: () => false } hook.addCommand(command1) expect(command1.number).to.equal(1) - const command2 = { event: true } + const command2 = { event: true, isMatchingEvent: () => false } hook.addCommand(command2) expect(command2.number).to.be.undefined @@ -42,13 +43,24 @@ describe('Hook model', () => { hook.addCommand(command3) expect(command3.number).to.equal(2) }) + + it('adds command as duplicate if it matches the last command', () => { + const addDuplicate = sinon.spy() + const command1 = { event: true, isMatchingEvent: () => true, addDuplicate } + hook.addCommand(command1) + + const command2 = { event: true } + hook.addCommand(command2) + + expect(addDuplicate).to.be.calledWith(command2) + }) }) context('#commandMatchingErr', () => { it('returns last command to match the error', () => { - const matchesButIsntLast = { err: { displayMessage: 'matching error message' } } + const matchesButIsntLast = { err: { displayMessage: 'matching error message' }, isMatchingEvent: () => false } hook.addCommand(matchesButIsntLast) - const doesntMatch = { err: { displayMessage: 'other error message' } } + const doesntMatch = { err: { displayMessage: 'other error message' }, isMatchingEvent: () => false } hook.addCommand(doesntMatch) const matches = { err: { displayMessage: 'matching error message' } } hook.addCommand(matches) @@ -57,7 +69,7 @@ describe('Hook model', () => { }) it('returns undefined when no match', () => { - const noMatch1 = { err: { displayMessage: 'some error message' } } + const noMatch1 = { err: { displayMessage: 'some error message' }, isMatchingEvent: () => false } hook.addCommand(noMatch1) const noMatch2 = { err: { displayMessage: 'other error message' } } hook.addCommand(noMatch2) diff --git a/packages/reporter/src/runnables/runnables.scss b/packages/reporter/src/runnables/runnables.scss index ea697d473188..9ad1158d4109 100644 --- a/packages/reporter/src/runnables/runnables.scss +++ b/packages/reporter/src/runnables/runnables.scss @@ -15,7 +15,6 @@ .runnables { padding-left: 0; - user-select: none; } .runnable { @@ -72,6 +71,7 @@ line-height: 18px; margin-right: 5px; width: 12px; + text-align: center; } &.suite .collapsible-indicator { @@ -95,20 +95,30 @@ } } - &.suite.runnable-pending > div > .runnable-wrapper { - border-left: 10px solid lighten($pending, 25%); - } - + &.suite.runnable-pending > div > .runnable-wrapper, &.test.runnable-pending > .runnable-wrapper { border-left: 10px solid lighten($pending, 25%); } + &.suite.runnable-passed > div > .runnable-wrapper, &.test.runnable-passed > .runnable-wrapper { border-left: 10px solid $pass; } - &.suite.runnable-passed > div > .runnable-wrapper { - border-left: 10px solid $pass; + &.runnable-skipped > .runnable-wrapper { + .runnable-state { + @extend .#{$fa-css-prefix}-ban; + color: #888; + } + + .runnable-title { + color: #aaa; + } + } + + &.suite.runnable-skipped > div > .runnable-wrapper, + &.test.runnable-skipped > .runnable-wrapper { + border-left: 10px solid #9a9aaa; } &.suite.runnable-failed > div > .runnable-wrapper { diff --git a/packages/reporter/src/test/test-model.spec.js b/packages/reporter/src/test/test-model.spec.js index 5b80fb271752..90542547d746 100644 --- a/packages/reporter/src/test/test-model.spec.js +++ b/packages/reporter/src/test/test-model.spec.js @@ -81,7 +81,7 @@ describe('Test model', () => { it('adds the command to an existing hook if it already exists', () => { const test = new Test({}) - test.addCommand({}, 'some hook') + test.addCommand({ isMatchingEvent: () => false }, 'some hook') expect(test.hooks.length).to.equal(1) expect(test.hooks[0].commands.length).to.equal(1) test.addCommand({}, 'some hook') diff --git a/packages/runner/jsconfig.json b/packages/runner/jsconfig.json new file mode 100644 index 000000000000..504cd646e149 --- /dev/null +++ b/packages/runner/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + } +} diff --git a/packages/runner/src/app/app.jsx b/packages/runner/src/app/app.jsx index d5b21c04898c..0605c2ffb56a 100644 --- a/packages/runner/src/app/app.jsx +++ b/packages/runner/src/app/app.jsx @@ -15,7 +15,6 @@ import Header from '../header/header' import Iframes from '../iframe/iframes' import Message from '../message/message' import Resizer from './resizer' -import RunnerWrap from './runner-wrap' @observer class App extends Component { @@ -41,15 +40,16 @@ class App extends Component { error={errorMessages.reporterError(this.props.state.scriptError, specPath)} /> </div> - <RunnerWrap - className='container' + <div + ref='container' + className='runner container' style={{ left: this.props.state.absoluteReporterWidth }} > <Header ref='header' {...this.props} /> - <Iframes {...this.props} /> - <Message state={this.props.state} /> + <Iframes ref='iframes' {...this.props} /> + <Message ref='message' state={this.props.state} /> {this.props.children} - </RunnerWrap> + </div> <Resizer style={{ left: this.props.state.absoluteReporterWidth }} state={this.props.state} @@ -57,12 +57,17 @@ class App extends Component { onResize={this._onReporterResize} onResizeEnd={this._onReporterResizeEnd} /> + {/* these pixels help ensure the browser has painted when taking a screenshot */} + <div ref='screenshotHelperPixels' className='screenshot-helper-pixels'> + <div /><div /><div /><div /><div /><div /> + </div> </div> ) } componentDidMount () { this._monitorWindowResize() + this._handleScreenshots() } _specPath () { @@ -104,6 +109,100 @@ class App extends Component { }) } + _handleScreenshots () { + const containerNode = findDOMNode(this.refs.container) + const reporterNode = this.refs.reporterWrap + const headerNode = findDOMNode(this.refs.header) + const iframesNode = findDOMNode(this.refs.iframes) + const iframesSizeNode = findDOMNode(this.refs.iframes.getSizeContainer()) + const screenshotHelperPixels = this.refs.screenshotHelperPixels + + let prevAttrs = {} + let screenshotting = false + + const { eventManager } = this.props + + eventManager.on('before:screenshot', (config) => { + if (!config.appOnly) return + + screenshotting = true + + prevAttrs = { + config, + top: iframesNode.style.top, + marginLeft: iframesSizeNode.style.marginLeft, + width: iframesSizeNode.style.width, + height: iframesSizeNode.style.height, + transform: iframesSizeNode.style.transform, + left: containerNode.style.left, + } + + const messageNode = findDOMNode(this.refs.message) + if (messageNode) { + messageNode.style.display = 'none' + } + + reporterNode.style.display = 'none' + headerNode.style.display = 'none' + + iframesNode.style.top = 0 + iframesNode.style.backgroundColor = 'black' + iframesSizeNode.style.marginLeft = 0 + + containerNode.style.left = 0 + containerNode.className += ' screenshotting' + + if (!config.scale) { + const $window = $(window) + const $iframesSizeNode = $(iframesSizeNode) + iframesSizeNode.style.width = `${Math.min($window.width(), $iframesSizeNode.width())}px` + iframesSizeNode.style.height = `${Math.min($window.height(), $iframesSizeNode.height())}px` + iframesSizeNode.style.transform = null + } + + screenshotHelperPixels.style.display = 'none' + }) + + const afterScreenshot = (config) => { + if (!config.appOnly) return + + screenshotting = false + + screenshotHelperPixels.style.display = 'block' + + containerNode.className = containerNode.className.replace(' screenshotting', '') + containerNode.style.left = prevAttrs.left + + iframesNode.style.top = prevAttrs.top + iframesNode.style.backgroundColor = null + iframesSizeNode.style.marginLeft = prevAttrs.marginLeft + + reporterNode.style.display = null + headerNode.style.display = null + + const messageNode = findDOMNode(this.refs.message) + if (messageNode) { + messageNode.style.display = null + } + + if (!config.scale) { + iframesSizeNode.style.transform = prevAttrs.transform + iframesSizeNode.style.width = prevAttrs.width + iframesSizeNode.style.height = prevAttrs.height + } + + prevAttrs = {} + } + + eventManager.on('after:screenshot', afterScreenshot) + + eventManager.on('run:start', () => { + if (screenshotting) { + afterScreenshot(prevAttrs.config) + } + }) + } + componentWillUnmount () { $(this.props.window).off('resize', this._onWindowResize) } diff --git a/packages/runner/src/app/app.scss b/packages/runner/src/app/app.scss index 0b5d35997510..94d0bd1b8e91 100644 --- a/packages/runner/src/app/app.scss +++ b/packages/runner/src/app/app.scss @@ -37,6 +37,16 @@ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1IiBoZWlnaHQ9IjUiPgo8cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiBmaWxsPSIjOWU5ZTllIj48L3JlY3Q+CjxwYXRoIGQ9Ik0wIDVMNSAwWk02IDRMNCA2Wk0tMSAxTDEgLTFaIiBzdHJva2U9IiM4ODgiIHN0cm9rZS13aWR0aD0iMSI+PC9wYXRoPgo8L3N2Zz4="); } + + &.screenshotting { + background: #fff; + box-shadow: none; + left: 0; + + .aut-iframe { + box-shadow: none; + } + } } .runner-resizer { @@ -46,3 +56,48 @@ top: 0; width: 5px; } + +.screenshot-helper-pixels { + div { + height: 1px; + width: 1px; + position: fixed; + z-index: 10; + } + + div:nth-child(1) { + background: #ccc; + left: 0; + top: 0; + } + + div:nth-child(2) { + background: white; + left: 1px; + top: 0; + } + + div:nth-child(3) { + background: white; + left: 0; + top: 1px; + } + + div:nth-child(4) { + background: white; + right: 0; + top: 0; + } + + div:nth-child(5) { + background: white; + bottom: 0; + left: 0; + } + + div:nth-child(6) { + background: black; + bottom: 0; + right: 0; + } +} diff --git a/packages/runner/src/app/app.spec.jsx b/packages/runner/src/app/app.spec.jsx index fa6d63fda8e4..2cebae276a48 100644 --- a/packages/runner/src/app/app.spec.jsx +++ b/packages/runner/src/app/app.spec.jsx @@ -27,7 +27,6 @@ const createProps = () => ({ emit: sinon.spy(), on: sinon.spy(), }, - on: sinon.spy(), }, state: new State(), windowUtil: { @@ -63,11 +62,11 @@ describe('<App />', () => { expect(component.find(Reporter)).to.have.prop('autoScrollingEnabled', true) }) - it('renders the runner wrap with `left` set as the width of the reporter', () => { + it('renders the runner container with `left` set as the width of the reporter', () => { const props = createProps() props.state.absoluteReporterWidth = 400 const component = shallow(<App {...props} />) - expect(component.find('RunnerWrap').prop('style').left).to.equal(400) + expect(component.find('.runner').prop('style').left).to.equal(400) }) it('renders the <Header />', () => { diff --git a/packages/runner/src/app/runner-wrap.jsx b/packages/runner/src/app/runner-wrap.jsx deleted file mode 100644 index e1d43540096b..000000000000 --- a/packages/runner/src/app/runner-wrap.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' - -const RunnerWrap = (props) => ( - <div {...props} className={`runner ${props.className}`}> - {props.children} - </div> -) - -export default RunnerWrap diff --git a/packages/runner/src/errors/automation-disconnected.jsx b/packages/runner/src/errors/automation-disconnected.jsx index 25f0d0790b34..a444977fc5a0 100644 --- a/packages/runner/src/errors/automation-disconnected.jsx +++ b/packages/runner/src/errors/automation-disconnected.jsx @@ -1,8 +1,7 @@ import React from 'react' -import RunnerWrap from '../app/runner-wrap' export default ({ onReload }) => ( - <RunnerWrap className='automation-failure'> + <div className='runner automation-failure'> <div className='automation-message automation-disconnected'> <p>Whoops, the Cypress Chrome extension has disconnected.</p> <p className='muted'>Cypress cannot run tests without this extension.</p> @@ -16,5 +15,5 @@ export default ({ onReload }) => ( </a> </div> </div> - </RunnerWrap> + </div> ) diff --git a/packages/runner/src/errors/no-automation.jsx b/packages/runner/src/errors/no-automation.jsx index 321168b3ffba..34449e0cc24f 100644 --- a/packages/runner/src/errors/no-automation.jsx +++ b/packages/runner/src/errors/no-automation.jsx @@ -2,7 +2,6 @@ import _ from 'lodash' import React from 'react' import Dropdown from '../dropdown/dropdown' -import RunnerWrap from '../app/runner-wrap' const displayName = (name) => _.capitalize(name) @@ -59,7 +58,7 @@ const browserPicker = (browsers, onLaunchBrowser) => { } export default ({ browsers, onLaunchBrowser }) => ( - <RunnerWrap className='automation-failure'> + <div className='runner automation-failure'> <div className='automation-message'> <p>Whoops, we can't run your tests.</p> {browsers.length ? browserPicker(browsers, onLaunchBrowser) : noBrowsers()} @@ -69,5 +68,5 @@ export default ({ browsers, onLaunchBrowser }) => ( </a> </div> </div> - </RunnerWrap> + </div> ) diff --git a/packages/runner/src/errors/no-spec.jsx b/packages/runner/src/errors/no-spec.jsx index b777d3e319be..5c70d1ea519b 100644 --- a/packages/runner/src/errors/no-spec.jsx +++ b/packages/runner/src/errors/no-spec.jsx @@ -1,5 +1,4 @@ import React, { Component } from 'react' -import RunnerWrap from '../app/runner-wrap' import eventManager from '../lib/event-manager' class NoSpec extends Component { @@ -9,7 +8,7 @@ class NoSpec extends Component { render () { return ( - <RunnerWrap className='no-spec'> + <div className='runner no-spec'> <div className='no-spec-message'> <p>Whoops, there is no test to run.</p> <p className='muted'>Choose a test to run from the desktop application.</p> @@ -22,7 +21,7 @@ class NoSpec extends Component { <img src={`/${this.props.config.namespace}/runner/no-spec-instructions.png`} /> </div> {this.props.children} - </RunnerWrap> + </div> ) } diff --git a/packages/runner/src/header/header.jsx b/packages/runner/src/header/header.jsx index 5a67309ee99c..943c566ffb8c 100644 --- a/packages/runner/src/header/header.jsx +++ b/packages/runner/src/header/header.jsx @@ -51,7 +51,7 @@ export default class Header extends Component { <ul className='menu'> <li className={cs('viewport-info', { 'open': this.showingViewportMenu })}> <button onClick={this._toggleViewportMenu}> - {state.width} x {state.height} <span className='viewport-scale'>({state.displayScale}%)</span> + {state.width} <span className='the-x'>x</span> {state.height} <span className='viewport-scale'>({state.displayScale}%)</span> <i className='fa fa-fw fa-info-circle'></i> </button> <div className='viewport-menu'> diff --git a/packages/runner/src/header/header.scss b/packages/runner/src/header/header.scss index fafea13a5a7a..64aa33ab85b6 100644 --- a/packages/runner/src/header/header.scss +++ b/packages/runner/src/header/header.scss @@ -420,6 +420,12 @@ color :#333; } } + + // HACK: Do not change this. It preloads the font to prevent rendering + // issues in the reporter when taking screenshots + .the-x { + font-family: $open-sans; + } } .viewport-scale { diff --git a/packages/runner/src/iframe/aut-iframe.js b/packages/runner/src/iframe/aut-iframe.js index b24af3cd7d9f..e6dae766d281 100644 --- a/packages/runner/src/iframe/aut-iframe.js +++ b/packages/runner/src/iframe/aut-iframe.js @@ -346,4 +346,36 @@ export default class AutIframe { Yielded: Cypress.dom.getElements($el), }) } + + beforeScreenshot = (config) => { + // could fail if iframe is cross-origin, so fail gracefully + try { + if (config.disableTimersAndAnimations) { + dom.addCssAnimationDisabler(this._body()) + } + _.each(config.blackout, (selector) => { + dom.addBlackout(this._body(), selector) + }) + } catch (err) { + /* eslint-disable no-console */ + console.error('Failed to modify app dom:') + console.error(err) + /* eslint-disable no-console */ + } + } + + afterScreenshot = (config) => { + // could fail if iframe is cross-origin, so fail gracefully + try { + if (config.disableTimersAndAnimations) { + dom.removeCssAnimationDisabler(this._body()) + } + dom.removeBlackouts(this._body()) + } catch (err) { + /* eslint-disable no-console */ + console.error('Failed to modify app dom:') + console.error(err) + /* eslint-disable no-console */ + } + } } diff --git a/packages/runner/src/iframe/iframes.jsx b/packages/runner/src/iframe/iframes.jsx index 84bf25f86dc9..606c1b46962f 100644 --- a/packages/runner/src/iframe/iframes.jsx +++ b/packages/runner/src/iframe/iframes.jsx @@ -48,16 +48,18 @@ export default class Iframes extends Component { this.autIframe = new AutIframe(this.props.config) - eventManager.on('visit:failed', this.autIframe.showVisitFailure) - eventManager.on('script:error', this._setScriptError) + this.props.eventManager.on('visit:failed', this.autIframe.showVisitFailure) + this.props.eventManager.on('before:screenshot', this.autIframe.beforeScreenshot) + this.props.eventManager.on('after:screenshot', this.autIframe.afterScreenshot) + this.props.eventManager.on('script:error', this._setScriptError) // TODO: need to take headless mode into account // may need to not display reporter if more than 200 tests - eventManager.on('restart', () => { + this.props.eventManager.on('restart', () => { this._run(this.props.config, specPath) }) - eventManager.on('print:selector:elements:to:console', this._printSelectorElementsToConsole) + this.props.eventManager.on('print:selector:elements:to:console', this._printSelectorElementsToConsole) this._disposers.push(autorun(() => { this.autIframe.toggleSelectorPlayground(selectorPlaygroundModel.isEnabled) @@ -67,7 +69,7 @@ export default class Iframes extends Component { this.autIframe.toggleSelectorHighlight(selectorPlaygroundModel.isShowingHighlight) })) - eventManager.start(this.props.config, specPath) + this.props.eventManager.start(this.props.config, specPath) this.iframeModel = new IframeModel({ state: this.props.state, @@ -82,7 +84,7 @@ export default class Iframes extends Component { }, snapshotControls: (snapshotProps) => ( <SnapshotControls - eventManager={eventManager} + eventManager={this.props.eventManager} snapshotProps={snapshotProps} state={this.props.state} onToggleHighlights={this._toggleSnapshotHighlights} @@ -103,11 +105,11 @@ export default class Iframes extends Component { logger.clearLog() this._setScriptError(null) - eventManager.setup(config, specPath) + this.props.eventManager.setup(config, specPath) this._loadIframes(specPath) .then(($autIframe) => { - eventManager.initialize($autIframe, config) + this.props.eventManager.initialize($autIframe, config) }) } @@ -170,9 +172,13 @@ export default class Iframes extends Component { componentWillUnmount () { this.props.eventManager.notifyRunningSpec(null) - eventManager.stop() + this.props.eventManager.stop() this._disposers.forEach((dispose) => { dispose() }) } + + getSizeContainer () { + return this.refs.container + } } diff --git a/packages/runner/src/lib/dom.js b/packages/runner/src/lib/dom.js index 925b71c3e622..7916cc79fae4 100644 --- a/packages/runner/src/lib/dom.js +++ b/packages/runner/src/lib/dom.js @@ -4,10 +4,14 @@ import Promise from 'bluebird' import selectorPlaygroundHighlight from '../selector-playground/highlight' +const styles = (styleString) => { + return styleString.replace(/\s*\n\s*/g, '') +} + const resetStyles = ` - border: none !important - margin: 0 !important - padding: 0 !important + border: none !important; + margin: 0 !important; + padding: 0 !important; ` function addHitBoxLayer (coords, body) { @@ -27,34 +31,32 @@ function addHitBoxLayer (coords, body) { const dotTop = height / 2 - dotHeight / 2 const dotLeft = width / 2 - dotWidth / 2 - const box = $('<div class="__cypress-highlight">', { - style: ` - ${resetStyles} - position: absolute - top: ${top}px - left: ${left}px - width: ${width}px - height: ${height}px - background-color: red - border-radius: 5px - box-shadow: 0 0 5px #333 - z-index: 2147483647 - `, - }) - const wrapper = $('<div>', { style: `${resetStyles} position: relative` }).appendTo(box) - $('<div>', { - style: ` - ${resetStyles} - position: absolute - top: ${dotTop}px - left: ${dotLeft}px - height: ${dotHeight}px - width: ${dotWidth}px - height: ${dotHeight}px - background-color: pink - border-radius: 5px - `, - }).appendTo(wrapper) + const boxStyles = styles(` + ${resetStyles} + position: absolute; + top: ${top}px; + left: ${left}px; + width: ${width}px; + height: ${height}px; + background-color: red; + border-radius: 5px; + box-shadow: 0 0 5px #333; + z-index: 2147483647; + `) + const box = $(`<div class="__cypress-highlight" style="${boxStyles}" />`) + const wrapper = $(`<div style="${styles(resetStyles)} position: relative" />`).appendTo(box) + const dotStyles = styles(` + ${resetStyles} + position: absolute; + top: ${dotTop}px; + left: ${dotLeft}px; + height: ${dotHeight}px; + width: ${dotWidth}px; + height: ${dotHeight}px; + background-color: pink; + border-radius: 5px; + `) + $(`<div style="${dotStyles}">`).appendTo(wrapper) return box.appendTo(body) } @@ -106,7 +108,7 @@ function addElementBoxModelLayers ($el, body) { obj.left -= dimensions.marginLeft } - // bail if the dimesions of this layer match the previous one + // bail if the dimensions of this layer match the previous one // so we dont create unnecessary layers if (dimensionsMatchPreviousLayer(obj, container)) return @@ -378,10 +380,63 @@ function getElementsForSelector ({ root, selector, method, cypressDom }) { return $el } +function addCssAnimationDisabler ($body) { + $(` + <style id="__cypress-animation-disabler"> + *, *:before, *:after { + transition-property: none !important; + animation: none !important; + } + </style> + `).appendTo($body) +} + +function removeCssAnimationDisabler ($body) { + $body.find('#__cypress-animation-disabler').remove() +} + +function addBlackout ($body, selector) { + let $el + try { + $el = $body.find(selector) + if (!$el.length) return + } catch (err) { + // if it's an invalid selector, just ignore it + return + } + + const dimensions = getElementDimensions($el) + const width = dimensions.widthWithBorder + const height = dimensions.heightWithBorder + const top = dimensions.offset.top + const left = dimensions.offset.left + + const style = styles(` + ${resetStyles} + position: absolute; + top: ${top}px; + left: ${left}px; + width: ${width}px; + height: ${height}px; + background-color: black; + z-index: 2147483647; + `) + + $(`<div class="__cypress-blackout" style="${style}">`).appendTo($body) +} + +function removeBlackouts ($body) { + $body.find('.__cypress-blackout').remove() +} + export default { + addBlackout, + removeBlackouts, addElementBoxModelLayers, addHitBoxLayer, addOrUpdateSelectorPlaygroundHighlight, + addCssAnimationDisabler, + removeCssAnimationDisabler, getElementsForSelector, getOuterSize, scrollIntoView, diff --git a/packages/runner/src/lib/event-manager.js b/packages/runner/src/lib/event-manager.js index 05b56f97a8bf..d3338c242f4e 100644 --- a/packages/runner/src/lib/event-manager.js +++ b/packages/runner/src/lib/event-manager.js @@ -21,7 +21,7 @@ channel.on('connect', () => { const driverToReporterEvents = 'paused'.split(' ') const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ') const driverToSocketEvents = 'backend:request automation:request mocha'.split(' ') -const driverTestEvents = 'test:before:run:async test:after:run test:set:state'.split(' ') +const driverTestEvents = 'test:before:run:async test:after:run'.split(' ') const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed'.split(' ') const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') @@ -236,6 +236,24 @@ const eventManager = { reporterBus.emit('reporter:log:state:changed', displayProps) }) + Cypress.on('before:screenshot', (config, cb) => { + const beforeThenCb = () => { + localBus.emit('before:screenshot', config) + cb() + } + + const wait = !config.appOnly && config.waitForCommandSynchronization + if (!config.appOnly) { + reporterBus.emit('test:set:state', _.pick(config, 'id', 'isOpen'), wait ? beforeThenCb : undefined) + } + if (!wait) beforeThenCb() + }) + + Cypress.on('after:screenshot', (config, cb) => { + localBus.emit('after:screenshot', config) + cb() + }) + _.each(driverToReporterEvents, (event) => { Cypress.on(event, (...args) => { reporterBus.emit(event, ...args) diff --git a/packages/runner/src/lib/shared.scss b/packages/runner/src/lib/shared.scss index b89bc4cd96e2..1b625d9e085e 100644 --- a/packages/runner/src/lib/shared.scss +++ b/packages/runner/src/lib/shared.scss @@ -2,8 +2,8 @@ * These styles are shared between the runner and the reporter */ -.num-elements { - background-color: #476fc9; +.num-elements, +.num-duplicates { border-radius: 5px; color: #fff; display: none; @@ -15,3 +15,11 @@ vertical-align: baseline; white-space: nowrap; } + +.num-elements { + background-color: #476fc9; +} + +.num-duplicates { + background-color: #999; +} diff --git a/packages/runner/src/lib/state.js b/packages/runner/src/lib/state.js index 147998d786b0..28e258a487d1 100644 --- a/packages/runner/src/lib/state.js +++ b/packages/runner/src/lib/state.js @@ -102,7 +102,7 @@ export default class State { this.isLoading = isLoading } - updateDimensions (width, height) { + @action updateDimensions (width, height) { this.width = width this.height = height } @@ -114,7 +114,7 @@ export default class State { if (headerHeight != null) this.headerHeight = headerHeight } - clearMessage () { + @action clearMessage () { this.messageTitle = _defaults.messageTitle this.messageDescription = _defaults.messageDescription this.messageType = _defaults.messageType @@ -128,7 +128,7 @@ export default class State { } } - resetUrl () { + @action resetUrl () { this.url = _defaults.url this.highlightUrl = _defaults.highlightUrl this.isLoadingUrl = _defaults.isLoadingUrl diff --git a/packages/runner/src/lib/variables.scss b/packages/runner/src/lib/variables.scss index ad7c9b40c0c9..0041e22ad1b5 100644 --- a/packages/runner/src/lib/variables.scss +++ b/packages/runner/src/lib/variables.scss @@ -4,3 +4,4 @@ $reporter-min-width: 450px; $message-height: 33px; $font-mono: Consolas, Monaco, 'Andale Mono', monospace; $font-sans: "Muli", "Helvetica Neue", "Arial", sans-serif; +$open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; diff --git a/packages/runner/static/index.html b/packages/runner/static/index.html index 93d2aaeb1557..df0fcdc5efda 100644 --- a/packages/runner/static/index.html +++ b/packages/runner/static/index.html @@ -13,6 +13,9 @@ <div id="app"></div> <script type="text/javascript" src="/__cypress/runner/cypress_runner.js"></script> <script type="text/javascript"> + // set a global so we know the 'top' window + window.__Cypress__ = true + setTimeout(function(){ Runner.start(document.getElementById('app'), {{{config}}}) }, 0) diff --git a/packages/server/README.md b/packages/server/README.md index f8d4eb9845ae..e427bae00865 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -53,6 +53,9 @@ Because of the large number of dependencies of the server, it's much more perfor ```bash ## runs only this one test file npm run test ./test/unit/api_spec.coffee + +## works for integration tests too +npm run test ./test/integration/server_spec.coffee ``` You can also run in `watch` mode @@ -61,3 +64,10 @@ You can also run in `watch` mode ## runs and watches only this one test file npm run test-watch ./test/unit/api_spec.coffee ``` + +To run an individual e2e test: + +```bash +## runs tests that match "base_url" +npm run test-e2e -- --spec base_url +``` diff --git a/packages/server/__snapshots__/async_timeouts_spec.coffee b/packages/server/__snapshots__/async_timeouts_spec.coffee index 813585951480..541dacaf0f91 100644 --- a/packages/server/__snapshots__/async_timeouts_spec.coffee +++ b/packages/server/__snapshots__/async_timeouts_spec.coffee @@ -1,7 +1,19 @@ exports['e2e async timeouts failing1 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (async_timeouts_spec.coffee) │ + │ Searched: cypress/integration/async_timeouts_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: async_timeouts_spec.coffee... (1 of 1) async @@ -18,16 +30,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: async_timeouts_spec.coffee │ + └──────────────────────────────────────────┘ (Screenshots) @@ -38,10 +53,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ async_timeouts_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/base_url_spec.coffee b/packages/server/__snapshots__/base_url_spec.coffee index 3be1d688fcf0..194e609ebfec 100644 --- a/packages/server/__snapshots__/base_url_spec.coffee +++ b/packages/server/__snapshots__/base_url_spec.coffee @@ -1,7 +1,19 @@ exports['e2e baseUrl https passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (base_url_spec.coffee) │ + │ Searched: cypress/integration/base_url_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: base_url_spec.coffee... (1 of 1) base url @@ -11,32 +23,56 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 1 passing - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: base_url_spec.coffee │ + └────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ base_url_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` exports['e2e baseUrl http passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (base_url_spec.coffee) │ + │ Searched: cypress/integration/base_url_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: base_url_spec.coffee... (1 of 1) base url @@ -46,25 +82,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 1 passing - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: base_url_spec.coffee │ + └────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ base_url_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` diff --git a/packages/server/__snapshots__/blacklist_hosts_spec.coffee b/packages/server/__snapshots__/blacklist_hosts_spec.coffee index 913eb2147a74..d7b894beb0f2 100644 --- a/packages/server/__snapshots__/blacklist_hosts_spec.coffee +++ b/packages/server/__snapshots__/blacklist_hosts_spec.coffee @@ -1,7 +1,19 @@ exports['e2e blacklist passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (blacklist_hosts_spec.coffee) │ + │ Searched: cypress/integration/blacklist_hosts_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: blacklist_hosts_spec.coffee... (1 of 1) blacklist @@ -11,25 +23,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 1 passing - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: blacklist_hosts_spec.coffee │ + └───────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ blacklist_hosts_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` diff --git a/packages/server/__snapshots__/browserify_babel_es2015_spec.coffee b/packages/server/__snapshots__/browserify_babel_es2015_spec.coffee index 1bff5b0f82b8..f65fb304ddb9 100644 --- a/packages/server/__snapshots__/browserify_babel_es2015_spec.coffee +++ b/packages/server/__snapshots__/browserify_babel_es2015_spec.coffee @@ -1,7 +1,19 @@ exports['e2e browserify, babel, es2015 passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (browserify_babel_es2015_passing_spec.coffee) │ + │ Searched: cypress/integration/browserify_babel_es2015_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: browserify_babel_es2015_passing_spec.coffee... (1 of 1) imports work @@ -13,32 +25,57 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 3 passing - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 3 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 3 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: browserify_babel_es2015_passing_spec.coffee │ + └───────────────────────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + +==================================================================================================== - (All Done) + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ browserify_babel_es2015_passing_spec.… Xs 3 3 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 3 3 - - - ` exports['e2e browserify, babel, es2015 fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (browserify_babel_es2015_failing_spec.js) │ + │ Searched: cypress/integration/browserify_babel_es2015_failing_spec.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: browserify_babel_es2015_failing_spec.js... (1 of 1) - (Tests Starting) Oops...we found an error preparing this test file: /foo/bar/.projects/e2e/cypress/integration/browserify_babel_es2015_failing_spec.js @@ -57,25 +94,37 @@ This occurred while Cypress was compiling and bundling your test code. This is u Fix the error in your code and re-run your tests. - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────┐ + │ Tests: 0 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: browserify_babel_es2015_failing_spec.js │ + └───────────────────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ browserify_babel_es2015_failing_spec.… Xs - - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs - - 1 - - ` diff --git a/packages/server/__snapshots__/busted_support_file_spec.coffee b/packages/server/__snapshots__/busted_support_file_spec.coffee index 3e087ed9f30a..ecd4ea4833e4 100644 --- a/packages/server/__snapshots__/busted_support_file_spec.coffee +++ b/packages/server/__snapshots__/busted_support_file_spec.coffee @@ -1,7 +1,19 @@ exports['e2e busted support file passes 1'] = ` -Started video recording: /foo/bar/.projects/busted-support-file/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.coffee) │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.coffee... (1 of 1) - (Tests Starting) Oops...we found an error preparing this test file: /foo/bar/.projects/busted-support-file/cypress/support/index.js @@ -18,25 +30,37 @@ This occurred while Cypress was compiling and bundling your test code. This is u Fix the error in your code and re-run your tests. - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────┐ + │ Tests: 0 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.coffee │ + └───────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/busted-support-file/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/busted-support-file/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ app_spec.coffee Xs - - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs - - 1 - - ` diff --git a/packages/server/__snapshots__/cache_spec.coffee b/packages/server/__snapshots__/cache_spec.coffee index 2a903e000da2..159e272db490 100644 --- a/packages/server/__snapshots__/cache_spec.coffee +++ b/packages/server/__snapshots__/cache_spec.coffee @@ -1,7 +1,19 @@ exports['e2e cache passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (cache_spec.coffee) │ + │ Searched: cypress/integration/cache_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: cache_spec.coffee... (1 of 1) caching @@ -14,25 +26,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 4 passing - (Tests Finished) + (Results) - - Tests: 4 - - Passes: 4 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────┐ + │ Tests: 4 │ + │ Passing: 4 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: cache_spec.coffee │ + └─────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ cache_spec.coffee Xs 4 4 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 4 4 - - - ` diff --git a/packages/server/__snapshots__/caught_uncaught_hook_errors_spec.coffee b/packages/server/__snapshots__/caught_uncaught_hook_errors_spec.coffee index 97200c8789ec..53bc951bf941 100644 --- a/packages/server/__snapshots__/caught_uncaught_hook_errors_spec.coffee +++ b/packages/server/__snapshots__/caught_uncaught_hook_errors_spec.coffee @@ -1,7 +1,19 @@ exports['e2e caught and uncaught hooks errors failing1 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (hook_caught_error_failing_spec.coffee) │ + │ Searched: cypress/integration/hook_caught_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: hook_caught_error_failing_spec.coffee... (1 of 1) ✓ t1a @@ -60,16 +72,19 @@ Because this error occurred during a 'before all' hook we are skipping the remai - (Tests Finished) + (Results) - - Tests: 5 - - Passes: 5 - - Failures: 3 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 3 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────────────────────┐ + │ Tests: 11 │ + │ Passing: 5 │ + │ Failing: 3 │ + │ Pending: 0 │ + │ Skipped: 3 │ + │ Screenshots: 3 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: hook_caught_error_failing_spec.coffee │ + └─────────────────────────────────────────────────────┘ (Screenshots) @@ -82,17 +97,38 @@ Because this error occurred during a 'before all' hook we are skipping the remai (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ hook_caught_error_failing_spec.coffee Xs 11 5 3 - 3 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 11 5 3 - 3 ` exports['e2e caught and uncaught hooks errors failing2 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (hook_uncaught_error_failing_spec.coffee) │ + │ Searched: cypress/integration/hook_uncaught_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: hook_uncaught_error_failing_spec.coffee... (1 of 1) ✓ t1b @@ -125,16 +161,19 @@ Because this error occurred during a 'before each' hook we are skipping the rema - (Tests Finished) + (Results) - - Tests: 4 - - Passes: 4 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────┐ + │ Tests: 7 │ + │ Passing: 4 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 2 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: hook_uncaught_error_failing_spec.coffee │ + └───────────────────────────────────────────────────────┘ (Screenshots) @@ -145,17 +184,38 @@ Because this error occurred during a 'before each' hook we are skipping the rema (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ hook_uncaught_error_failing_spec.coff… Xs 7 4 1 - 2 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 7 4 1 - 2 ` exports['e2e caught and uncaught hooks errors failing3 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (hook_uncaught_root_error_failing_spec.coffee) │ + │ Searched: cypress/integration/hook_uncaught_root_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: hook_uncaught_root_error_failing_spec.coffee... (1 of 1) 1) "before each" hook for "t1c" @@ -180,16 +240,19 @@ Because this error occurred during a 'before each' hook we are skipping all of t - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────────────────────────┐ + │ Tests: 4 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 3 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: hook_uncaught_root_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────┘ (Screenshots) @@ -200,17 +263,38 @@ Because this error occurred during a 'before each' hook we are skipping all of t (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ hook_uncaught_root_error_failing_spec… Xs 4 - 1 - 3 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 4 - 1 - 3 ` exports['e2e caught and uncaught hooks errors failing4 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (hook_uncaught_error_events_failing_spec.coffee) │ + │ Searched: cypress/integration/hook_uncaught_error_events_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: hook_uncaught_error_events_failing_spec.coffee... (1 of 1) uncaught hook error should continue to fire all mocha events @@ -241,16 +325,19 @@ Because this error occurred during a 'before each' hook we are skipping the rema - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 2 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: hook_uncaught_error_events_failing_spec.coffee │ + └──────────────────────────────────────────────────────────────┘ (Screenshots) @@ -261,10 +348,19 @@ Because this error occurred during a 'before each' hook we are skipping the rema (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ hook_uncaught_error_events_failing_sp… Xs 3 2 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 3 2 1 - - ` diff --git a/packages/server/__snapshots__/commands_outside_of_test_spec.coffee b/packages/server/__snapshots__/commands_outside_of_test_spec.coffee index 8f93d4790e47..ee7f208cc8bc 100644 --- a/packages/server/__snapshots__/commands_outside_of_test_spec.coffee +++ b/packages/server/__snapshots__/commands_outside_of_test_spec.coffee @@ -1,7 +1,19 @@ exports['e2e commands outside of test fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (commands_outside_of_test_spec.coffee) │ + │ Searched: cypress/integration/commands_outside_of_test_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: commands_outside_of_test_spec.coffee... (1 of 1) 1) An uncaught error was detected outside of a test @@ -38,16 +50,19 @@ We dynamically generated a new test to display this failure. - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: commands_outside_of_test_spec.coffee │ + └────────────────────────────────────────────────────┘ (Screenshots) @@ -58,10 +73,19 @@ We dynamically generated a new test to display this failure. (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ commands_outside_of_test_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/config_spec.coffee b/packages/server/__snapshots__/config_spec.coffee index 4d9d891affb2..4fad4138d499 100644 --- a/packages/server/__snapshots__/config_spec.coffee +++ b/packages/server/__snapshots__/config_spec.coffee @@ -1,7 +1,19 @@ exports['e2e config passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (config_passing_spec.coffee) │ + │ Searched: cypress/integration/config_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: config_passing_spec.coffee... (1 of 1) Cypress.config() @@ -13,32 +25,56 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 3 passing - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 3 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 3 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: config_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ config_passing_spec.coffee Xs 3 3 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 3 3 - - - ` exports['e2e config fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (config_failing_spec.coffee) │ + │ Searched: cypress/integration/config_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: config_failing_spec.coffee... (1 of 1) config @@ -68,16 +104,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: config_failing_spec.coffee │ + └──────────────────────────────────────────┘ (Screenshots) @@ -88,10 +127,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ config_failing_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/cookies_spec.coffee b/packages/server/__snapshots__/cookies_spec.coffee index 2b72483bcad7..e23bbaeb266f 100644 --- a/packages/server/__snapshots__/cookies_spec.coffee +++ b/packages/server/__snapshots__/cookies_spec.coffee @@ -1,7 +1,19 @@ exports['e2e cookies passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (cookies_spec.coffee) │ + │ Searched: cypress/integration/cookies_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: cookies_spec.coffee... (1 of 1) cookies @@ -16,25 +28,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 6 passing - (Tests Finished) + (Results) - - Tests: 6 - - Passes: 6 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────┐ + │ Tests: 6 │ + │ Passing: 6 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: cookies_spec.coffee │ + └───────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ cookies_spec.coffee Xs 6 6 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 6 6 - - - ` diff --git a/packages/server/__snapshots__/domain_spec.coffee b/packages/server/__snapshots__/domain_spec.coffee index 4c167cfc2b86..a6b08b954789 100644 --- a/packages/server/__snapshots__/domain_spec.coffee +++ b/packages/server/__snapshots__/domain_spec.coffee @@ -1,7 +1,19 @@ exports['e2e domain passing 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (domain_spec.coffee) │ + │ Searched: cypress/integration/domain_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: domain_spec.coffee... (1 of 1) localhost @@ -17,25 +29,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 3 passing - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 3 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 3 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: domain_spec.coffee │ + └──────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ domain_spec.coffee Xs 3 3 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 3 3 - - - ` diff --git a/packages/server/__snapshots__/form_submissions_spec.coffee b/packages/server/__snapshots__/form_submissions_spec.coffee index 85c5618e80af..4b8f9b948ec5 100644 --- a/packages/server/__snapshots__/form_submissions_spec.coffee +++ b/packages/server/__snapshots__/form_submissions_spec.coffee @@ -1,7 +1,19 @@ exports['e2e form submissions passing 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (form_submission_passing_spec.coffee) │ + │ Searched: cypress/integration/form_submission_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: form_submission_passing_spec.coffee... (1 of 1) form submissions @@ -12,32 +24,56 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: form_submission_passing_spec.coffee │ + └───────────────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ form_submission_passing_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` exports['e2e form submissions failing 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (form_submission_failing_spec.coffee) │ + │ Searched: cypress/integration/form_submission_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: form_submission_failing_spec.coffee... (1 of 1) form submission fails @@ -68,16 +104,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: form_submission_failing_spec.coffee │ + └───────────────────────────────────────────────────┘ (Screenshots) @@ -88,10 +127,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ form_submission_failing_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/go_spec.coffee b/packages/server/__snapshots__/go_spec.coffee index 7dc6712a5ea9..86ba9d7f74aa 100644 --- a/packages/server/__snapshots__/go_spec.coffee +++ b/packages/server/__snapshots__/go_spec.coffee @@ -1,7 +1,19 @@ exports['e2e go passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (go_spec.coffee) │ + │ Searched: cypress/integration/go_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: go_spec.coffee... (1 of 1) cy.go @@ -12,25 +24,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: go_spec.coffee │ + └──────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ go_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` diff --git a/packages/server/__snapshots__/iframe_spec.coffee b/packages/server/__snapshots__/iframe_spec.coffee index c71d76848a90..dce9bd92b6cb 100644 --- a/packages/server/__snapshots__/iframe_spec.coffee +++ b/packages/server/__snapshots__/iframe_spec.coffee @@ -1,7 +1,19 @@ exports['e2e iframes passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (iframe_spec.coffee) │ + │ Searched: cypress/integration/iframe_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: iframe_spec.coffee... (1 of 1) iframes @@ -18,25 +30,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 8 passing - (Tests Finished) + (Results) - - Tests: 8 - - Passes: 8 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────┐ + │ Tests: 8 │ + │ Passing: 8 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: iframe_spec.coffee │ + └──────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ iframe_spec.coffee Xs 8 8 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 8 8 - - - ` diff --git a/packages/server/__snapshots__/images_spec.coffee b/packages/server/__snapshots__/images_spec.coffee index eedb817bdd8b..1bc8dec5bfb2 100644 --- a/packages/server/__snapshots__/images_spec.coffee +++ b/packages/server/__snapshots__/images_spec.coffee @@ -1,7 +1,19 @@ exports['e2e images passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (images_spec.coffee) │ + │ Searched: cypress/integration/images_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: images_spec.coffee... (1 of 1) images @@ -12,25 +24,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: images_spec.coffee │ + └──────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ images_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` diff --git a/packages/server/__snapshots__/issue_149_spec.coffee b/packages/server/__snapshots__/issue_149_spec.coffee index c31c811acaba..42790adad609 100644 --- a/packages/server/__snapshots__/issue_149_spec.coffee +++ b/packages/server/__snapshots__/issue_149_spec.coffee @@ -1,7 +1,19 @@ exports['e2e issue 149 failing 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (issue_149_spec.coffee) │ + │ Searched: cypress/integration/issue_149_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: issue_149_spec.coffee... (1 of 1) 1) fails @@ -17,16 +29,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 1 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: issue_149_spec.coffee │ + └─────────────────────────────────────┘ (Screenshots) @@ -37,10 +52,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ issue_149_spec.coffee Xs 2 1 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 1 1 - - ` diff --git a/packages/server/__snapshots__/issue_173_spec.coffee b/packages/server/__snapshots__/issue_173_spec.coffee index d0c872cb6205..c51c6ac97059 100644 --- a/packages/server/__snapshots__/issue_173_spec.coffee +++ b/packages/server/__snapshots__/issue_173_spec.coffee @@ -1,7 +1,19 @@ exports['e2e issue 173 failing 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (issue_173_spec.coffee) │ + │ Searched: cypress/integration/issue_173_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: issue_173_spec.coffee... (1 of 1) 1) fails @@ -30,16 +42,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 1 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: issue_173_spec.coffee │ + └─────────────────────────────────────┘ (Screenshots) @@ -50,10 +65,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ issue_173_spec.coffee Xs 2 1 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 1 1 - - ` diff --git a/packages/server/__snapshots__/issue_674_spec.coffee b/packages/server/__snapshots__/issue_674_spec.coffee index d48360c6335b..27a63e36a516 100644 --- a/packages/server/__snapshots__/issue_674_spec.coffee +++ b/packages/server/__snapshots__/issue_674_spec.coffee @@ -1,7 +1,19 @@ exports['e2e issue 674 fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (issue_674_spec.coffee) │ + │ Searched: cypress/integration/issue_674_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: issue_674_spec.coffee... (1 of 1) issue 674 @@ -27,16 +39,19 @@ Because this error occurred during a 'after each' hook we are skipping the remai - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 2 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 2 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 2 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: issue_674_spec.coffee │ + └─────────────────────────────────────┘ (Screenshots) @@ -48,10 +63,19 @@ Because this error occurred during a 'after each' hook we are skipping the remai (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ issue_674_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/js_error_handling_spec.coffee b/packages/server/__snapshots__/js_error_handling_spec.coffee index 95a1f455073a..9b54d93b509b 100644 --- a/packages/server/__snapshots__/js_error_handling_spec.coffee +++ b/packages/server/__snapshots__/js_error_handling_spec.coffee @@ -1,7 +1,19 @@ exports['e2e js error handling fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (js_error_handling_failing_spec.coffee) │ + │ Searched: cypress/integration/js_error_handling_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: js_error_handling_failing_spec.coffee... (1 of 1) s1 @@ -88,16 +100,19 @@ https://on.cypress.io/uncaught-exception-from-application - (Tests Finished) + (Results) - - Tests: 7 - - Passes: 2 - - Failures: 5 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 5 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────────────────────┐ + │ Tests: 7 │ + │ Passing: 2 │ + │ Failing: 5 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 5 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: js_error_handling_failing_spec.coffee │ + └─────────────────────────────────────────────────────┘ (Screenshots) @@ -112,10 +127,19 @@ https://on.cypress.io/uncaught-exception-from-application (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ js_error_handling_failing_spec.coffee Xs 7 2 5 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 7 2 5 - - ` diff --git a/packages/server/__snapshots__/new_project_spec.coffee b/packages/server/__snapshots__/new_project_spec.coffee index adc366e5cfa1..bc92dea711fe 100644 --- a/packages/server/__snapshots__/new_project_spec.coffee +++ b/packages/server/__snapshots__/new_project_spec.coffee @@ -1,7 +1,18 @@ exports['e2e new project passes 1'] = ` -Started video recording: /foo/bar/.projects/no-scaffolding/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.coffee) │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.coffee... (1 of 1) ✓ is true @@ -9,25 +20,37 @@ Started video recording: /foo/bar/.projects/no-scaffolding/cypress/videos/abc123 1 passing - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.coffee │ + └───────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/no-scaffolding/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/no-scaffolding/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` diff --git a/packages/server/__snapshots__/only_spec.coffee b/packages/server/__snapshots__/only_spec.coffee index f540908a5b82..27f6e68ff6e2 100644 --- a/packages/server/__snapshots__/only_spec.coffee +++ b/packages/server/__snapshots__/only_spec.coffee @@ -1,7 +1,19 @@ exports['e2e only spec failing 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (only_spec.coffee) │ + │ Searched: cypress/integration/only_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: only_spec.coffee... (1 of 1) s1 @@ -11,25 +23,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 1 passing - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: only_spec.coffee │ + └────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ only_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` diff --git a/packages/server/__snapshots__/page_loading_spec.coffee b/packages/server/__snapshots__/page_loading_spec.coffee index 1f1f3fc4c603..cccb3e98665d 100644 --- a/packages/server/__snapshots__/page_loading_spec.coffee +++ b/packages/server/__snapshots__/page_loading_spec.coffee @@ -1,7 +1,19 @@ exports['e2e page_loading passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (page_loading_spec.coffee) │ + │ Searched: cypress/integration/page_loading_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: page_loading_spec.coffee... (1 of 1) page_loading @@ -13,25 +25,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: page_loading_spec.coffee │ + └────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ page_loading_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` diff --git a/packages/server/__snapshots__/plugins_spec.coffee b/packages/server/__snapshots__/plugins_spec.coffee index 33ecddbb0ef2..22398909f641 100644 --- a/packages/server/__snapshots__/plugins_spec.coffee +++ b/packages/server/__snapshots__/plugins_spec.coffee @@ -1,7 +1,19 @@ exports['e2e plugins fails 1'] = ` -Started video recording: /foo/bar/.projects/plugins-async-error/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.coffee) │ + │ Searched: cypress/integration/app_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.coffee... (1 of 1) Error: The following error was thrown by a plugin. We've stopped running your tests because a plugin crashed. @@ -23,32 +35,56 @@ Error: Async error from plugins file at stack trace line - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────┐ + │ Tests: 0 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.coffee │ + └───────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/plugins-async-error/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/plugins-async-error/cypress/videos/abc123.mp4 (X seconds) + +==================================================================================================== - (All Done) + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ app_spec.coffee Xs - - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs - - 1 - - ` exports['e2e plugins passes 1'] = ` -Started video recording: /foo/bar/.projects/working-preprocessor/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) - (Tests Starting) + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.coffee) │ + │ Searched: cypress/integration/app_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.coffee... (1 of 1) ✓ is another spec @@ -57,32 +93,56 @@ Started video recording: /foo/bar/.projects/working-preprocessor/cypress/videos/ 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.coffee │ + └───────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/working-preprocessor/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/working-preprocessor/cypress/videos/abc123.mp4 (X seconds) + +==================================================================================================== - (All Done) + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` exports['e2e plugins can modify config from plugins 1'] = ` -Started video recording: /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) - (Tests Starting) + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.coffee) │ + │ Searched: cypress/integration/app_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.coffee... (1 of 1) ✓ overrides config @@ -91,34 +151,62 @@ Started video recording: /foo/bar/.projects/plugin-config/cypress/videos/abc123. 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.coffee │ + └───────────────────────────────┘ (Video) - Started processing: Compressing to 20 CRF - - Finished processing: /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/plugin-config/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` -exports['e2e plugins works with user extensions 1'] = `Warning: Cypress can only record videos when using the built in 'electron' browser. +exports['e2e plugins works with user extensions 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (app_spec.coffee) │ + │ Searched: cypress/integration/app_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: app_spec.coffee... (1 of 1) + +Warning: Cypress can only record videos when using the built in 'electron' browser. You have set the browser to: 'chrome' A video will not be recorded when using this browser. - (Tests Starting) ✓ can inject text from an extension @@ -126,19 +214,31 @@ A video will not be recorded when using this browser. 1 passing - (Tests Finished) + (Results) + + ┌───────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: app_spec.coffee │ + └───────────────────────────────┘ + + +==================================================================================================== - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: false - - Cypress Version: 1.2.3 + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ app_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` diff --git a/packages/server/__snapshots__/promises_spec.coffee b/packages/server/__snapshots__/promises_spec.coffee index 6c1f37d7dfde..e0609e50ba60 100644 --- a/packages/server/__snapshots__/promises_spec.coffee +++ b/packages/server/__snapshots__/promises_spec.coffee @@ -1,7 +1,19 @@ exports['e2e promises failing1 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (promises_spec.coffee) │ + │ Searched: cypress/integration/promises_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: promises_spec.coffee... (1 of 1) 1) catches regular promise errors @@ -22,16 +34,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 0 - - Failures: 2 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 2 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 0 │ + │ Failing: 2 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 2 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: promises_spec.coffee │ + └────────────────────────────────────┘ (Screenshots) @@ -43,10 +58,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ promises_spec.coffee Xs 2 - 2 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 - 2 - - ` diff --git a/packages/server/__snapshots__/record_spec.coffee b/packages/server/__snapshots__/record_spec.coffee index 50dc37403a90..02f418c18816 100644 --- a/packages/server/__snapshots__/record_spec.coffee +++ b/packages/server/__snapshots__/record_spec.coffee @@ -1,66 +1,825 @@ -exports['lib/modes/record .generateProjectBuildId calls api.createRun with args 1'] = [ - { - "projectId": "id-123", - "recordKey": "key-123", - "commitSha": "sha-123", - "commitBranch": "master", - "commitAuthorName": "brian", - "commitAuthorEmail": "brian@cypress.io", - "commitMessage": "such hax", - "remoteOrigin": "https://github.com/foo/bar.git", - "ciParams": { - "foo": "bar" - }, - "ciProvider": "circle", - "ciBuildNumber": "build-123", - "groupId": null, - "specs": [ - "spec.js" - ] - } -] - -exports['lib/modes/record .generateProjectBuildId passes groupId 1'] = [ - { - "projectId": "id-123", - "recordKey": "key-123", - "commitSha": "sha-123", - "commitBranch": "master", - "commitAuthorName": "brian", - "commitAuthorEmail": "brian@cypress.io", - "commitMessage": "such hax", - "remoteOrigin": "https://github.com/foo/bar.git", - "ciParams": { - "foo": "bar" - }, - "ciProvider": "circle", - "ciBuildNumber": "build-123", - "groupId": "gr123", - "specs": [ - "spec.js" - ] - } -] - -exports['lib/modes/record .generateProjectBuildId figures out groupId from CI environment variables 1'] = [ - { - "projectId": "id-123", - "recordKey": "key-123", - "commitSha": "sha-123", - "commitBranch": "master", - "commitAuthorName": "brian", - "commitAuthorEmail": "brian@cypress.io", - "commitMessage": "such hax", - "remoteOrigin": "https://github.com/foo/bar.git", - "ciParams": { - "foo": "bar" - }, - "ciProvider": "circle", - "ciBuildNumber": "build-123", - "groupId": "ci-group-123", - "specs": [ - "spec.js" - ] - } -] +exports['e2e record passing passes 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 4 found (record_error_spec.coffee, record_fail_spec.coffee, record_pass_spec.coff… │ + │ Searched: cypress/integration/record* │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_error_spec.coffee... (1 of 4) + +Oops...we found an error preparing this test file: + + /foo/bar/.projects/e2e/cypress/integration/record_error_spec.coffee + +The error was: + +Error: Cannot find module '../it/does/not/exist' from '/foo/bar/.projects/e2e/cypress/integration' + + +This occurred while Cypress was compiling and bundling your test code. This is usually caused by: + +- A missing file or dependency +- A syntax error in the file or one of its dependencies + +Fix the error in your code and re-run your tests. + + (Results) + + ┌────────────────────────────────────────┐ + │ Tests: 0 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_error_spec.coffee │ + └────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + + (Uploading Results) + + - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_fail_spec.coffee... (2 of 4) + + + record fails + 1) "before each" hook for "fails 1" + + + 0 passing + 1 failing + + 1) record fails "before each" hook for "fails 1": + Error: foo + +Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'record fails' + at stack trace line + + + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 1 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_fail_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/record fails -- fails 1 -- before each hook.png (1280x720) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + + (Uploading Results) + + - Done Uploading (1/2) /foo/bar/.projects/e2e/cypress/screenshots/record fails -- fails 1 -- before each hook.png + - Done Uploading (2/2) /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (3 of 4) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + + (Uploading Results) + + - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_uncaught_spec.coffee... (4 of 4) + + + 1) An uncaught error was detected outside of a test + + 0 passing + 1 failing + + 1) An uncaught error was detected outside of a test: + Uncaught Error: instantly fails + +This error originated from your test code, not from Cypress. + +When Cypress detects uncaught errors originating from your test code it will automatically fail the current test. + +Cypress could not associate this error to any specific test. + +We dynamically generated a new test to display this failure. + at stack trace line + at stack trace line + at stack trace line + at stack trace line + + + + + (Results) + + ┌───────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_uncaught_spec.coffee │ + └───────────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/An uncaught error was detected outside of a test.png (1280x720) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + + (Uploading Results) + + - Done Uploading (1/2) /foo/bar/.projects/e2e/cypress/screenshots/An uncaught error was detected outside of a test.png + - Done Uploading (2/2) /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ record_error_spec.coffee Xs - - 1 - - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✖ record_fail_spec.coffee Xs 2 - 1 - 1 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✖ record_uncaught_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 3 of 4 failed (75%) Xs 5 1 3 1 1 + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + +` + +exports['e2e record api interaction errors recordKey and projectId errors and exits 1'] = `We failed trying to authenticate this project: pid123 + +Your Record Key is invalid: f858a...ee7e1 + +It may have been recently revoked by you or another user. + +Please log into the Dashboard to see the updated token. + +https://on.cypress.io/dashboard/projects/pid123 +` + +exports['e2e record api interaction errors project 404 errors and exits 1'] = `We could not find a project with the ID: pid123 + +This projectId came from your cypress.json file or an environment variable. + +Please log into the Dashboard and find your project. + +We will list the correct projectId in the 'Settings' tab. + +Alternatively, you can create a new project using the Desktop Application. + +https://on.cypress.io/dashboard +` + +exports['e2e record api interaction errors create run warns and does not create or update instances 1'] = `Warning: We encountered an error talking to our servers. + +This run will not be recorded. + +This error will not alter the exit code. + +StatusCodeError: 500 - "Internal Server Error" + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + +` + +exports['e2e record api interaction errors create instance does not update instance 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + +Warning: We encountered an error talking to our servers. + +This run will not be recorded. + +This error will not alter the exit code. + +StatusCodeError: 500 - "Internal Server Error" + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + +` + +exports['e2e record api interaction errors update instance does not update instance stdout 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + + (Uploading Results) + +Warning: We encountered an error talking to our servers. + +This run will not be recorded. + +This error will not alter the exit code. + +StatusCodeError: 500 - "Internal Server Error" + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + +` + +exports['e2e record api interaction errors update instance stdout warns but proceeds 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + + (Uploading Results) + + - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png +Warning: We encountered an error talking to our servers. + +This run will not be recorded. + +This error will not alter the exit code. + +StatusCodeError: 500 - "Internal Server Error" + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + +` + +exports['e2e record failing errors and exits without projectId 1'] = `You passed the --record flag but this project has not been setup to record. + +This project is missing the 'projectId' inside of 'cypress.json'. + +We cannot uniquely identify this project without this id. + +You need to setup this project to record. This will generate a unique 'projectId'. + +Alternatively if you omit the --record flag this project will run without recording. + +https://on.cypress.io/recording-project-runs +` + +exports['e2e record recordKey errors and exits without recordKey 1'] = `You passed the --record flag but did not provide us your Record Key. + +You can pass us your Record Key like this: + + cypress run --record --key <record_key> + +You can also set the key as an environment variable with the name CYPRESS_RECORD_KEY. + +https://on.cypress.io/how-do-i-record-runs +` + +exports['e2e record projectId errors and exits without projectId 1'] = `You passed the --record flag but this project has not been setup to record. + +This project is missing the 'projectId' inside of 'cypress.json'. + +We cannot uniquely identify this project without this id. + +You need to setup this project to record. This will generate a unique 'projectId'. + +Alternatively if you omit the --record flag this project will run without recording. + +https://on.cypress.io/recording-project-runs +` + +exports['e2e record api interaction errors recordKey and projectId errors and exits on 401 1'] = `We failed trying to authenticate this project: pid123 + +Your Record Key is invalid: f858a...ee7e1 + +It may have been recently revoked by you or another user. + +Please log into the Dashboard to see the updated token. + +https://on.cypress.io/dashboard/projects/pid123 +` + +exports['e2e record video recording does not upload when not enabled 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + + (Uploading Results) + + - Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + +` + +exports['e2e record api interaction errors uploading assets warns but proceeds 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + │ Run URL: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + + (Uploading Results) + + - Failed Uploading (1/2) /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png + - Failed Uploading (2/2) /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + + +─────────────────────────────────────────────────────────────────────────────────────────────────────── + + Recorded Run: https://dashboard.cypress.io/#/projects/cjvoj7/runs/12 + +` + +exports['e2e record misconfiguration errors and exits when no browser found 1'] = `Can't run because you've entered an invalid browser. + +Browser: 'browserDoesNotExist' was not found on your system. + +Available browsers found are: browser1, browser2, browser3 +` + +exports['e2e record misconfiguration errors and exits when no specs found 1'] = `Can't run because no spec files were found. + +We searched for any files matching this glob pattern: + +cypress/integration/notfound/** +` + +exports['e2e record recordKey warns but does not exit when is forked pr 1'] = `Warning: It looks like you are trying to record this run from a forked PR. + +The 'Record Key' is missing. Your CI provider is likely not passing private environment variables to builds from forks. + +These results will not be recorded. + +This error will not alter the exit code. + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (record_pass_spec.coffee) │ + │ Searched: cypress/integration/record_pass* │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: record_pass_spec.coffee... (1 of 1) + + + record pass + ✓ passes + - is pending + + + 1 passing + 1 pending + + + (Results) + + ┌───────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 1 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: record_pass_spec.coffee │ + └───────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/yay it passes.png (202x1002) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ record_pass_spec.coffee Xs 2 1 - 1 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 1 - 1 - + +` diff --git a/packages/server/__snapshots__/reporter_spec.coffee b/packages/server/__snapshots__/reporter_spec.coffee index 2c60ddf4284f..5d5fdae8205a 100644 --- a/packages/server/__snapshots__/reporter_spec.coffee +++ b/packages/server/__snapshots__/reporter_spec.coffee @@ -1,23 +1,60 @@ -exports['lib/reporter #stats has reporterName and failingTests in stats 1'] = { +exports['lib/reporter #stats has reporterName stats, reporterStats, etc 1'] = { + "stats": { + "suites": 2, + "tests": 2, + "passes": 0, + "pending": 1, + "skipped": 0, + "failures": 1, + "wallClockDuration": 0 + }, "reporter": "foo", - "failingTests": [ + "reporterStats": { + "suites": 0, + "tests": 1, + "passes": 0, + "pending": 0, + "failures": 1 + }, + "hooks": [], + "tests": [ { - "clientId": "r4", - "title": "TodoMVC - React /// When page is initially opened /// should focus on the todo input field", - "duration": 4, + "testId": "r4", + "title": [ + "TodoMVC - React", + "When page is initially opened", + "should focus on the todo input field" + ], + "state": "failed", + "body": "", "stack": [ 1, 2, 3 ], "error": "foo", - "started": 1234 + "timings": null, + "failedFromHookId": null, + "wallClockStartedAt": null, + "wallClockDuration": null, + "videoTimestamp": null + }, + { + "testId": "r5", + "title": [ + "TodoMVC - React", + "When page is initially opened", + "does something good" + ], + "state": "pending", + "body": "", + "stack": null, + "error": null, + "timings": null, + "failedFromHookId": null, + "wallClockStartedAt": null, + "wallClockDuration": null, + "videoTimestamp": null } - ], - "suites": 0, - "tests": 1, - "passes": 0, - "pending": 0, - "failures": 1 + ] } - diff --git a/packages/server/__snapshots__/reporters_spec.coffee b/packages/server/__snapshots__/reporters_spec.coffee index 08d07d3fb230..58551241ba2e 100644 --- a/packages/server/__snapshots__/reporters_spec.coffee +++ b/packages/server/__snapshots__/reporters_spec.coffee @@ -5,69 +5,133 @@ We searched for the reporter in these paths: - /foo/bar/.projects/e2e/module-does-not-exist - /foo/bar/.projects/e2e/node_modules/module-does-not-exist +The error we received was: + +Cannot find module '/foo/bar/.projects/e2e/node_modules/module-does-not-exist' + Learn more at stack trace line ` exports['e2e reporters supports junit reporter and reporter options 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_passing_spec.coffee) │ + │ Searched: cypress/integration/simple_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_passing_spec.coffee... (1 of 1) - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_passing_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` exports['e2e reporters supports local custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_passing_spec.coffee) │ + │ Searched: cypress/integration/simple_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_passing_spec.coffee... (1 of 1) passes finished! - (Tests Finished) + (Results) - - Tests: undefined - - Passes: undefined - - Failures: undefined - - Pending: undefined - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_passing_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` exports['e2e reporters mochawesome passes with mochawesome@1.5.2 npm custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) - (Tests Starting) + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_passing_spec.coffee) │ + │ Searched: cypress/integration/simple_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_passing_spec.coffee... (1 of 1) [mochawesome] Generating report files... @@ -83,48 +147,100 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_passing_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` exports['e2e reporters mochawesome fails with mochawesome@1.5.2 npm custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_failing_hook_spec.coffee) │ + │ Searched: cypress/integration/simple_failing_hook_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_failing_hook_spec.coffee... (1 of 1) [mochawesome] Generating report files... simple failing hook spec - 1) "before each" hook for "never gets here" + beforeEach hooks + 1) "before each" hook for "never gets here" + pending + - is pending + afterEach hooks + ✓ runs this + 2) "after each" hook for "runs this" + after hooks + ✓ runs this + ✓ fails on this + 3) "after all" hook for "fails on this" + + + 3 passing + 1 pending + 3 failing + + 1) simple failing hook spec + beforeEach hooks + "before each" hook for "never gets here": + Error: fail1 +Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'beforeEach hooks' + at stack trace line - 0 passing - 1 failing + 2) simple failing hook spec + afterEach hooks + "after each" hook for "runs this": + Error: fail2 - 1) simple failing hook spec - "before each" hook for "never gets here": - Error: fail +Because this error occurred during a 'after each' hook we are skipping the remaining tests in the current suite: 'afterEach hooks' + at stack trace line + + 3) simple failing hook spec + after hooks + "after all" hook for "fails on this": + Error: fail3 -Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'simple failing hook spec' +Because this error occurred during a 'after all' hook we are skipping the remaining tests in the current suite: 'after hooks' at stack trace line @@ -134,37 +250,63 @@ Because this error occurred during a 'before each' hook we are skipping the rema - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────┐ + │ Tests: 6 │ + │ Passing: 1 │ + │ Failing: 3 │ + │ Pending: 1 │ + │ Skipped: 1 │ + │ Screenshots: 3 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_failing_hook_spec.coffee │ + └───────────────────────────────────────────────┘ (Screenshots) - - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- never gets here -- before each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- beforeEach hooks -- never gets here -- before each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- afterEach hooks -- runs this -- after each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- after hooks -- fails on this -- after all hook.png (1280x720) (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ simple_failing_hook_spec.coffee Xs 6 1 3 1 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 6 1 3 1 1 ` exports['e2e reporters mochawesome passes with mochawesome@2.3.1 npm custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_passing_spec.coffee) │ + │ Searched: cypress/integration/simple_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_passing_spec.coffee... (1 of 1) simple passing spec @@ -178,46 +320,98 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 [mochawesome] Report HTML saved to /foo/bar/.projects/e2e/mochawesome-report/mochawesome.html - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_passing_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` exports['e2e reporters mochawesome fails with mochawesome@2.3.1 npm custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_failing_hook_spec.coffee) │ + │ Searched: cypress/integration/simple_failing_hook_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - simple failing hook spec - 1) "before each" hook for "never gets here" + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_failing_hook_spec.coffee... (1 of 1) - 0 passing - 1 failing + simple failing hook spec + beforeEach hooks + 1) "before each" hook for "never gets here" + pending + - is pending + afterEach hooks + ✓ runs this + 2) "after each" hook for "runs this" + after hooks + ✓ runs this + ✓ fails on this + 3) "after all" hook for "fails on this" + + + 3 passing + 1 pending + 3 failing 1) simple failing hook spec - "before each" hook for "never gets here": - Error: fail + beforeEach hooks + "before each" hook for "never gets here": + Error: fail1 + +Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'beforeEach hooks' + at stack trace line -Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'simple failing hook spec' + 2) simple failing hook spec + afterEach hooks + "after each" hook for "runs this": + Error: fail2 + +Because this error occurred during a 'after each' hook we are skipping the remaining tests in the current suite: 'afterEach hooks' + at stack trace line + + 3) simple failing hook spec + after hooks + "after all" hook for "fails on this": + Error: fail3 + +Because this error occurred during a 'after all' hook we are skipping the remaining tests in the current suite: 'after hooks' at stack trace line @@ -227,37 +421,63 @@ Because this error occurred during a 'before each' hook we are skipping the rema [mochawesome] Report HTML saved to /foo/bar/.projects/e2e/mochawesome-report/mochawesome.html - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────┐ + │ Tests: 6 │ + │ Passing: 1 │ + │ Failing: 3 │ + │ Pending: 1 │ + │ Skipped: 1 │ + │ Screenshots: 3 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_failing_hook_spec.coffee │ + └───────────────────────────────────────────────┘ (Screenshots) - - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- never gets here -- before each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- beforeEach hooks -- never gets here -- before each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- afterEach hooks -- runs this -- after each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- after hooks -- fails on this -- after all hook.png (1280x720) (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + +==================================================================================================== - (All Done) + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ simple_failing_hook_spec.coffee Xs 6 1 3 1 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 6 1 3 1 1 ` exports['e2e reporters mochawesome passes with mochawesome@3.0.1 npm custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_passing_spec.coffee) │ + │ Searched: cypress/integration/simple_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_passing_spec.coffee... (1 of 1) simple passing spec @@ -271,46 +491,98 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 [mochawesome] Report HTML saved to /foo/bar/.projects/e2e/mochawesome-report/mochawesome.html - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_passing_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` exports['e2e reporters mochawesome fails with mochawesome@3.0.1 npm custom reporter 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_failing_hook_spec.coffee) │ + │ Searched: cypress/integration/simple_failing_hook_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - simple failing hook spec - 1) "before each" hook for "never gets here" +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_failing_hook_spec.coffee... (1 of 1) - 0 passing - 1 failing + + simple failing hook spec + beforeEach hooks + 1) "before each" hook for "never gets here" + pending + - is pending + afterEach hooks + ✓ runs this + 2) "after each" hook for "runs this" + after hooks + ✓ runs this + ✓ fails on this + 3) "after all" hook for "fails on this" + + + 3 passing + 1 pending + 3 failing 1) simple failing hook spec - "before each" hook for "never gets here": - Error: fail + beforeEach hooks + "before each" hook for "never gets here": + Error: fail1 + +Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'beforeEach hooks' + at stack trace line + + 2) simple failing hook spec + afterEach hooks + "after each" hook for "runs this": + Error: fail2 -Because this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'simple failing hook spec' +Because this error occurred during a 'after each' hook we are skipping the remaining tests in the current suite: 'afterEach hooks' + at stack trace line + + 3) simple failing hook spec + after hooks + "after all" hook for "fails on this": + Error: fail3 + +Because this error occurred during a 'after all' hook we are skipping the remaining tests in the current suite: 'after hooks' at stack trace line @@ -320,30 +592,90 @@ Because this error occurred during a 'before each' hook we are skipping the rema [mochawesome] Report HTML saved to /foo/bar/.projects/e2e/mochawesome-report/mochawesome.html - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────┐ + │ Tests: 6 │ + │ Passing: 1 │ + │ Failing: 3 │ + │ Pending: 1 │ + │ Skipped: 1 │ + │ Screenshots: 3 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: simple_failing_hook_spec.coffee │ + └───────────────────────────────────────────────┘ (Screenshots) - - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- never gets here -- before each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- beforeEach hooks -- never gets here -- before each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- afterEach hooks -- runs this -- after each hook.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- after hooks -- fails on this -- after all hook.png (1280x720) (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ simple_failing_hook_spec.coffee Xs 6 1 3 1 1 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 6 1 3 1 1 + +` + +exports['e2e reporters reports error when thrown from reporter 1'] = `Could not load reporter by name: reporters/throws.js + +We searched for the reporter in these paths: + +- /foo/bar/.projects/e2e/reporters/throws.js +- /foo/bar/.projects/e2e/node_modules/reporters/throws.js + +The error we received was: + +Error: this reporter threw an error + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + + +Learn more at stack trace line ` diff --git a/packages/server/__snapshots__/request_spec.coffee b/packages/server/__snapshots__/request_spec.coffee index feacf2a349be..e117315f47aa 100644 --- a/packages/server/__snapshots__/request_spec.coffee +++ b/packages/server/__snapshots__/request_spec.coffee @@ -1,7 +1,19 @@ exports['e2e requests passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (request_spec.coffee) │ + │ Searched: cypress/integration/request_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: request_spec.coffee... (1 of 1) redirects + requests @@ -22,32 +34,56 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 12 passing - (Tests Finished) + (Results) - - Tests: 12 - - Passes: 12 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────┐ + │ Tests: 12 │ + │ Passing: 12 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: request_spec.coffee │ + └───────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ request_spec.coffee Xs 12 12 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 12 12 - - - ` exports['e2e requests fails when network immediately fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (request_http_network_error_failing_spec.coffee) │ + │ Searched: cypress/integration/request_http_network_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: request_http_network_error_failing_spec.coffee... (1 of 1) when network connection cannot be established @@ -121,16 +157,19 @@ RequestError: Error: connect ECONNREFUSED 127.0.0.1:16795 - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: request_http_network_error_failing_spec.coffee │ + └──────────────────────────────────────────────────────────────┘ (Screenshots) @@ -141,17 +180,38 @@ RequestError: Error: connect ECONNREFUSED 127.0.0.1:16795 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ request_http_network_error_failing_sp… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e requests fails on status code 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (request_status_code_failing_spec.coffee) │ + │ Searched: cypress/integration/request_status_code_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: request_status_code_failing_spec.coffee... (1 of 1) when status code isnt 2xx or 3xx @@ -218,16 +278,19 @@ Body: Service Unavailable - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: request_status_code_failing_spec.coffee │ + └───────────────────────────────────────────────────────┘ (Screenshots) @@ -238,10 +301,19 @@ Body: Service Unavailable (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ request_status_code_failing_spec.coff… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/return_value_spec.coffee b/packages/server/__snapshots__/return_value_spec.coffee index f26d32452035..ae9a12830719 100644 --- a/packages/server/__snapshots__/return_value_spec.coffee +++ b/packages/server/__snapshots__/return_value_spec.coffee @@ -1,7 +1,19 @@ exports['e2e return value failing1 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (return_value_spec.coffee) │ + │ Searched: cypress/integration/return_value_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: return_value_spec.coffee... (1 of 1) 1) errors when invoking commands and return a different value @@ -81,16 +93,19 @@ https://on.cypress.io/returning-value-and-commands-in-custom-command - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 0 - - Failures: 2 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 2 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 0 │ + │ Failing: 2 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 2 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: return_value_spec.coffee │ + └────────────────────────────────────────┘ (Screenshots) @@ -102,10 +117,19 @@ https://on.cypress.io/returning-value-and-commands-in-custom-command (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ return_value_spec.coffee Xs 2 - 2 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 - 2 - - ` diff --git a/packages/server/__snapshots__/scaffold_spec.coffee b/packages/server/__snapshots__/scaffold_spec.coffee index 524bdbe70ae5..1a7b9cce5b9a 100644 --- a/packages/server/__snapshots__/scaffold_spec.coffee +++ b/packages/server/__snapshots__/scaffold_spec.coffee @@ -3,7 +3,66 @@ exports['lib/scaffold .fileTree returns tree-like structure of scaffolded 1'] = "name": "tests", "children": [ { - "name": "example_spec.js" + "name": "examples", + "children": [ + { + "name": "actions.spec.js" + }, + { + "name": "aliasing.spec.js" + }, + { + "name": "assertions.spec.js" + }, + { + "name": "connectors.spec.js" + }, + { + "name": "cookies.spec.js" + }, + { + "name": "cypress_api.spec.js" + }, + { + "name": "files.spec.js" + }, + { + "name": "local_storage.spec.js" + }, + { + "name": "location.spec.js" + }, + { + "name": "misc.spec.js" + }, + { + "name": "navigation.spec.js" + }, + { + "name": "network_requests.spec.js" + }, + { + "name": "querying.spec.js" + }, + { + "name": "spies_stubs_clocks.spec.js" + }, + { + "name": "traversal.spec.js" + }, + { + "name": "utilities.spec.js" + }, + { + "name": "viewport.spec.js" + }, + { + "name": "waiting.spec.js" + }, + { + "name": "window.spec.js" + } + ] }, { "name": "_fixtures", @@ -46,7 +105,66 @@ exports['lib/scaffold .fileTree leaves out fixtures if configured to false 1'] = "name": "tests", "children": [ { - "name": "example_spec.js" + "name": "examples", + "children": [ + { + "name": "actions.spec.js" + }, + { + "name": "aliasing.spec.js" + }, + { + "name": "assertions.spec.js" + }, + { + "name": "connectors.spec.js" + }, + { + "name": "cookies.spec.js" + }, + { + "name": "cypress_api.spec.js" + }, + { + "name": "files.spec.js" + }, + { + "name": "local_storage.spec.js" + }, + { + "name": "location.spec.js" + }, + { + "name": "misc.spec.js" + }, + { + "name": "navigation.spec.js" + }, + { + "name": "network_requests.spec.js" + }, + { + "name": "querying.spec.js" + }, + { + "name": "spies_stubs_clocks.spec.js" + }, + { + "name": "traversal.spec.js" + }, + { + "name": "utilities.spec.js" + }, + { + "name": "viewport.spec.js" + }, + { + "name": "waiting.spec.js" + }, + { + "name": "window.spec.js" + } + ] }, { "name": "_support", @@ -81,7 +199,66 @@ exports['lib/scaffold .fileTree leaves out support if configured to false 1'] = "name": "tests", "children": [ { - "name": "example_spec.js" + "name": "examples", + "children": [ + { + "name": "actions.spec.js" + }, + { + "name": "aliasing.spec.js" + }, + { + "name": "assertions.spec.js" + }, + { + "name": "connectors.spec.js" + }, + { + "name": "cookies.spec.js" + }, + { + "name": "cypress_api.spec.js" + }, + { + "name": "files.spec.js" + }, + { + "name": "local_storage.spec.js" + }, + { + "name": "location.spec.js" + }, + { + "name": "misc.spec.js" + }, + { + "name": "navigation.spec.js" + }, + { + "name": "network_requests.spec.js" + }, + { + "name": "querying.spec.js" + }, + { + "name": "spies_stubs_clocks.spec.js" + }, + { + "name": "traversal.spec.js" + }, + { + "name": "utilities.spec.js" + }, + { + "name": "viewport.spec.js" + }, + { + "name": "waiting.spec.js" + }, + { + "name": "window.spec.js" + } + ] }, { "name": "_fixtures", @@ -162,7 +339,66 @@ exports['lib/scaffold .fileTree leaves out plugins if configured to false 1'] = "name": "tests", "children": [ { - "name": "example_spec.js" + "name": "examples", + "children": [ + { + "name": "actions.spec.js" + }, + { + "name": "aliasing.spec.js" + }, + { + "name": "assertions.spec.js" + }, + { + "name": "connectors.spec.js" + }, + { + "name": "cookies.spec.js" + }, + { + "name": "cypress_api.spec.js" + }, + { + "name": "files.spec.js" + }, + { + "name": "local_storage.spec.js" + }, + { + "name": "location.spec.js" + }, + { + "name": "misc.spec.js" + }, + { + "name": "navigation.spec.js" + }, + { + "name": "network_requests.spec.js" + }, + { + "name": "querying.spec.js" + }, + { + "name": "spies_stubs_clocks.spec.js" + }, + { + "name": "traversal.spec.js" + }, + { + "name": "utilities.spec.js" + }, + { + "name": "viewport.spec.js" + }, + { + "name": "waiting.spec.js" + }, + { + "name": "window.spec.js" + } + ] }, { "name": "_fixtures", diff --git a/packages/server/__snapshots__/screenshot_app_capture_spec.coffee b/packages/server/__snapshots__/screenshot_app_capture_spec.coffee new file mode 100644 index 000000000000..f1cf5ab379fe --- /dev/null +++ b/packages/server/__snapshots__/screenshot_app_capture_spec.coffee @@ -0,0 +1,112 @@ +exports['e2e screenshot app capture passes 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (screenshot_app_capture_spec.coffee) │ + │ Searched: cypress/integration/screenshot_app_capture_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: screenshot_app_capture_spec.coffee... (1 of 1) + + + ✓ takes consistent app captures + + 1 passing + + + (Results) + + ┌──────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 51 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: screenshot_app_capture_spec.coffee │ + └──────────────────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/app-original.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + - /foo/bar/.projects/e2e/cypress/screenshots/app-compare.png (1000x660) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ screenshot_app_capture_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - + +` + diff --git a/packages/server/__snapshots__/screenshot_element_capture_spec.coffee b/packages/server/__snapshots__/screenshot_element_capture_spec.coffee new file mode 100644 index 000000000000..7887b5f59f45 --- /dev/null +++ b/packages/server/__snapshots__/screenshot_element_capture_spec.coffee @@ -0,0 +1,72 @@ +exports['e2e screenshot element capture passes 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (screenshot_element_capture_spec.coffee) │ + │ Searched: cypress/integration/screenshot_element_capture_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: screenshot_element_capture_spec.coffee... (1 of 1) + + + ✓ takes consistent element captures + + 1 passing + + + (Results) + + ┌──────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 11 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: screenshot_element_capture_spec.coffee │ + └──────────────────────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/element-original.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + - /foo/bar/.projects/e2e/cypress/screenshots/element-compare.png (560x302) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ screenshot_element_capture_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - + +` + diff --git a/packages/server/__snapshots__/screenshot_fullpage_capture_spec.coffee b/packages/server/__snapshots__/screenshot_fullpage_capture_spec.coffee new file mode 100644 index 000000000000..e5599b5367f6 --- /dev/null +++ b/packages/server/__snapshots__/screenshot_fullpage_capture_spec.coffee @@ -0,0 +1,72 @@ +exports['e2e screenshot fullPage capture passes 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (screenshot_fullpage_capture_spec.coffee) │ + │ Searched: cypress/integration/screenshot_fullpage_capture_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: screenshot_fullpage_capture_spec.coffee... (1 of 1) + + + ✓ takes consistent fullPage captures + + 1 passing + + + (Results) + + ┌───────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 11 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: screenshot_fullpage_capture_spec.coffee │ + └───────────────────────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-original.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-compare.png (600x500) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ screenshot_fullpage_capture_spec.coff… Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - + +` + diff --git a/packages/server/__snapshots__/screenshots_spec.coffee b/packages/server/__snapshots__/screenshots_spec.coffee index 1f6ab1cda932..86f0acefbdd4 100644 --- a/packages/server/__snapshots__/screenshots_spec.coffee +++ b/packages/server/__snapshots__/screenshots_spec.coffee @@ -1,13 +1,35 @@ exports['e2e screenshots passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (screenshots_spec.coffee) │ + │ Searched: cypress/integration/screenshots_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: screenshots_spec.coffee... (1 of 1) taking screenshots ✓ manually generates pngs ✓ can nest screenshots in folders 1) generates pngs on failure + ✓ crops app captures to just app size + ✓ can capture fullPage screenshots + ✓ accepts subsequent same captures after multiple tries + ✓ accepts screenshot after multiple tries if somehow app has pixels that match helper pixels + ✓ can capture element screenshots + clipping + ✓ can clip app screenshots + ✓ can clip runner screenshots + ✓ can clip fullPage screenshots + ✓ can clip element screenshots before hooks 2) "before all" hook for "empty test 1" each hooks @@ -15,7 +37,7 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 4) "after each" hook for "empty test 2" - 2 passing + 11 passing 4 failing 1) taking screenshots generates pngs on failure: @@ -43,16 +65,19 @@ Because this error occurred during a 'after each' hook we are skipping the remai - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 2 - - Failures: 4 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 7 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────┐ + │ Tests: 14 │ + │ Passing: 11 │ + │ Failing: 3 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 16 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: screenshots_spec.coffee │ + └───────────────────────────────────────┘ (Screenshots) @@ -61,6 +86,15 @@ Because this error occurred during a 'after each' hook we are skipping the remai - /foo/bar/.projects/e2e/cypress/screenshots/red.png (1280x720) - /foo/bar/.projects/e2e/cypress/screenshots/foobarbaz.png (1280x720) - /foo/bar/.projects/e2e/cypress/screenshots/taking screenshots -- generates pngs on failure.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/crop-check.png (600x400) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-same.png (600x500) + - /foo/bar/.projects/e2e/cypress/screenshots/pathological.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/element.png (400x300) + - /foo/bar/.projects/e2e/cypress/screenshots/app-clip.png (100x50) + - /foo/bar/.projects/e2e/cypress/screenshots/runner-clip.png (120x60) + - /foo/bar/.projects/e2e/cypress/screenshots/fullPage-clip.png (140x70) + - /foo/bar/.projects/e2e/cypress/screenshots/element-clip.png (160x80) - /foo/bar/.projects/e2e/cypress/screenshots/taking screenshots -- before hooks -- empty test 1 -- before all hook.png (1280x720) - /foo/bar/.projects/e2e/cypress/screenshots/taking screenshots -- each hooks -- empty test 2 -- before each hook.png (1280x720) - /foo/bar/.projects/e2e/cypress/screenshots/taking screenshots -- each hooks -- empty test 2 -- after each hook.png (1280x720) @@ -69,10 +103,19 @@ Because this error occurred during a 'after each' hook we are skipping the remai (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ screenshots_spec.coffee Xs 14 11 3 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 14 11 3 - - ` diff --git a/packages/server/__snapshots__/spec_isolation_spec.coffee b/packages/server/__snapshots__/spec_isolation_spec.coffee new file mode 100644 index 000000000000..ad4b3d460774 --- /dev/null +++ b/packages/server/__snapshots__/spec_isolation_spec.coffee @@ -0,0 +1,617 @@ +exports['e2e spec_isolation failing 1'] = { + "startedTestsAt": "2018-02-01T20:14:19.323Z", + "endedTestsAt": "2018-02-01T20:14:19.323Z", + "totalDuration": 5555, + "totalSuites": 8, + "totalTests": 12, + "totalFailed": 5, + "totalPassed": 5, + "totalPending": 1, + "totalSkipped": 1, + "runs": [ + { + "stats": { + "suites": 5, + "tests": 6, + "passes": 1, + "pending": 1, + "skipped": 1, + "failures": 3, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockEndedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234 + }, + "reporter": "spec", + "reporterStats": { + "suites": 5, + "tests": 4, + "passes": 3, + "pending": 1, + "failures": 3, + "start": "2018-02-01T20:14:19.323Z", + "end": "2018-02-01T20:14:19.323Z", + "duration": 1234 + }, + "hooks": [ + { + "hookId": "h1", + "hookName": "before each", + "title": [ + "\"before each\" hook" + ], + "body": "function () {\n throw new Error(\"fail1\");\n }" + }, + { + "hookId": "h2", + "hookName": "after each", + "title": [ + "\"after each\" hook" + ], + "body": "function () {\n throw new Error(\"fail2\");\n }" + }, + { + "hookId": "h3", + "hookName": "after all", + "title": [ + "\"after all\" hook" + ], + "body": "function () {\n throw new Error(\"fail3\");\n }" + } + ], + "tests": [ + { + "testId": "r4", + "title": [ + "simple failing hook spec", + "beforeEach hooks", + "never gets here" + ], + "state": "failed", + "body": "function () {}", + "stack": "Error: fail1\n\nBecause this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'beforeEach hooks'\n at stack trace line", + "error": "fail1\n\nBecause this error occurred during a 'before each' hook we are skipping the remaining tests in the current suite: 'beforeEach hooks'", + "timings": { + "lifecycle": 100, + "before each": [ + { + "hookId": "h1", + "fnDuration": 400, + "afterFnDuration": 200 + } + ] + }, + "failedFromHookId": "h1", + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + }, + { + "testId": "r6", + "title": [ + "simple failing hook spec", + "pending", + "is pending" + ], + "state": "pending", + "body": "", + "stack": null, + "error": null, + "timings": null, + "failedFromHookId": null, + "wallClockStartedAt": null, + "wallClockDuration": null, + "videoTimestamp": null + }, + { + "testId": "r8", + "title": [ + "simple failing hook spec", + "afterEach hooks", + "runs this" + ], + "state": "failed", + "body": "function () {}", + "stack": "Error: fail2\n\nBecause this error occurred during a 'after each' hook we are skipping the remaining tests in the current suite: 'afterEach hooks'\n at stack trace line", + "error": "fail2\n\nBecause this error occurred during a 'after each' hook we are skipping the remaining tests in the current suite: 'afterEach hooks'", + "timings": { + "lifecycle": 100, + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + }, + "after each": [ + { + "hookId": "h2", + "fnDuration": 400, + "afterFnDuration": 200 + } + ] + }, + "failedFromHookId": "h2", + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + }, + { + "testId": "r9", + "title": [ + "simple failing hook spec", + "afterEach hooks", + "does not run this" + ], + "state": "skipped", + "body": "function () {}", + "stack": null, + "error": null, + "timings": null, + "failedFromHookId": null, + "wallClockStartedAt": null, + "wallClockDuration": null, + "videoTimestamp": null + }, + { + "testId": "r11", + "title": [ + "simple failing hook spec", + "after hooks", + "runs this" + ], + "state": "passed", + "body": "function () {}", + "stack": null, + "error": null, + "timings": { + "lifecycle": 100, + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + } + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + }, + { + "testId": "r12", + "title": [ + "simple failing hook spec", + "after hooks", + "fails on this" + ], + "state": "failed", + "body": "function () {}", + "stack": "Error: fail3\n\nBecause this error occurred during a 'after all' hook we are skipping the remaining tests in the current suite: 'after hooks'\n at stack trace line", + "error": "fail3\n\nBecause this error occurred during a 'after all' hook we are skipping the remaining tests in the current suite: 'after hooks'", + "timings": { + "lifecycle": 100, + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + }, + "after all": [ + { + "hookId": "h3", + "fnDuration": 400, + "afterFnDuration": 200 + } + ] + }, + "failedFromHookId": "h3", + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + } + ], + "error": null, + "video": "/foo/bar/.projects/e2e/cypress/videos/abc123.mp4", + "screenshots": [ + { + "screenshotId": "some-random-id", + "name": null, + "testId": "r4", + "takenAt": "2018-02-01T20:14:19.323Z", + "path": "/foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- beforeEach hooks -- never gets here -- before each hook.png", + "height": 720, + "width": 1280 + }, + { + "screenshotId": "some-random-id", + "name": null, + "testId": "r8", + "takenAt": "2018-02-01T20:14:19.323Z", + "path": "/foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- afterEach hooks -- runs this -- after each hook.png", + "height": 720, + "width": 1280 + }, + { + "screenshotId": "some-random-id", + "name": null, + "testId": "r12", + "takenAt": "2018-02-01T20:14:19.323Z", + "path": "/foo/bar/.projects/e2e/cypress/screenshots/simple failing hook spec -- after hooks -- fails on this -- after all hook.png", + "height": 720, + "width": 1280 + } + ], + "spec": { + "name": "simple_failing_hook_spec.coffee", + "path": "cypress/integration/simple_failing_hook_spec.coffee", + "absolute": "/foo/bar/.projects/e2e/cypress/integration/simple_failing_hook_spec.coffee" + }, + "shouldUploadVideo": true + }, + { + "stats": { + "suites": 1, + "tests": 2, + "passes": 0, + "pending": 0, + "skipped": 0, + "failures": 2, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockEndedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234 + }, + "reporter": "spec", + "reporterStats": { + "suites": 1, + "tests": 2, + "passes": 0, + "pending": 0, + "failures": 2, + "start": "2018-02-01T20:14:19.323Z", + "end": "2018-02-01T20:14:19.323Z", + "duration": 1234 + }, + "hooks": [], + "tests": [ + { + "testId": "r3", + "title": [ + "simple failing spec", + "fails1" + ], + "state": "failed", + "body": "function () {\n return cy.wrap(true, {\n timeout: 100\n }).should(\"be.false\");\n }", + "stack": "CypressError: Timed out retrying: expected true to be false\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line\n at stack trace line", + "error": "Timed out retrying: expected true to be false", + "timings": { + "lifecycle": 100, + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + } + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + }, + { + "testId": "r4", + "title": [ + "simple failing spec", + "fails2" + ], + "state": "failed", + "body": "function () {\n throw new Error(\"fails2\");\n }", + "stack": "Error: fails2\n at stack trace line", + "error": "fails2", + "timings": { + "lifecycle": 100, + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + } + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + } + ], + "error": null, + "video": "/foo/bar/.projects/e2e/cypress/videos/abc123.mp4", + "screenshots": [ + { + "screenshotId": "some-random-id", + "name": null, + "testId": "r3", + "takenAt": "2018-02-01T20:14:19.323Z", + "path": "/foo/bar/.projects/e2e/cypress/screenshots/simple failing spec -- fails1.png", + "height": 720, + "width": 1280 + }, + { + "screenshotId": "some-random-id", + "name": null, + "testId": "r4", + "takenAt": "2018-02-01T20:14:19.323Z", + "path": "/foo/bar/.projects/e2e/cypress/screenshots/simple failing spec -- fails2.png", + "height": 720, + "width": 1280 + } + ], + "spec": { + "name": "simple_failing_spec.coffee", + "path": "cypress/integration/simple_failing_spec.coffee", + "absolute": "/foo/bar/.projects/e2e/cypress/integration/simple_failing_spec.coffee" + }, + "shouldUploadVideo": true + }, + { + "stats": { + "suites": 1, + "tests": 3, + "passes": 3, + "pending": 0, + "skipped": 0, + "failures": 0, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockEndedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234 + }, + "reporter": "spec", + "reporterStats": { + "suites": 1, + "tests": 3, + "passes": 3, + "pending": 0, + "failures": 0, + "start": "2018-02-01T20:14:19.323Z", + "end": "2018-02-01T20:14:19.323Z", + "duration": 1234 + }, + "hooks": [ + { + "hookId": "h1", + "hookName": "before all", + "title": [ + "\"before all\" hook" + ], + "body": "function () {\n return cy.wait(100);\n }" + }, + { + "hookId": "h2", + "hookName": "before each", + "title": [ + "\"before each\" hook" + ], + "body": "function () {\n return cy.wait(200);\n }" + }, + { + "hookId": "h3", + "hookName": "after each", + "title": [ + "\"after each\" hook" + ], + "body": "function () {\n return cy.wait(200);\n }" + }, + { + "hookId": "h4", + "hookName": "after all", + "title": [ + "\"after all\" hook" + ], + "body": "function () {\n return cy.wait(100);\n }" + } + ], + "tests": [ + { + "testId": "r3", + "title": [ + "simple hooks spec", + "t1" + ], + "state": "passed", + "body": "function () {\n return cy.wrap(\"t1\").should(\"eq\", \"t1\");\n }", + "stack": null, + "error": null, + "timings": { + "lifecycle": 100, + "before all": [ + { + "hookId": "h1", + "fnDuration": 400, + "afterFnDuration": 200 + } + ], + "before each": [ + { + "hookId": "h2", + "fnDuration": 400, + "afterFnDuration": 200 + } + ], + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": 400, + "afterFnDuration": 200 + } + ] + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + }, + { + "testId": "r4", + "title": [ + "simple hooks spec", + "t2" + ], + "state": "passed", + "body": "function () {\n return cy.wrap(\"t2\").should(\"eq\", \"t2\");\n }", + "stack": null, + "error": null, + "timings": { + "lifecycle": 100, + "before each": [ + { + "hookId": "h2", + "fnDuration": 400, + "afterFnDuration": 200 + } + ], + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": 400, + "afterFnDuration": 200 + } + ] + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + }, + { + "testId": "r5", + "title": [ + "simple hooks spec", + "t3" + ], + "state": "passed", + "body": "function () {\n return cy.wrap(\"t3\").should(\"eq\", \"t3\");\n }", + "stack": null, + "error": null, + "timings": { + "lifecycle": 100, + "before each": [ + { + "hookId": "h2", + "fnDuration": 400, + "afterFnDuration": 200 + } + ], + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + }, + "after each": [ + { + "hookId": "h3", + "fnDuration": 400, + "afterFnDuration": 200 + } + ], + "after all": [ + { + "hookId": "h4", + "fnDuration": 400, + "afterFnDuration": 200 + } + ] + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + } + ], + "error": null, + "video": "/foo/bar/.projects/e2e/cypress/videos/abc123.mp4", + "screenshots": [], + "spec": { + "name": "simple_hooks_spec.coffee", + "path": "cypress/integration/simple_hooks_spec.coffee", + "absolute": "/foo/bar/.projects/e2e/cypress/integration/simple_hooks_spec.coffee" + }, + "shouldUploadVideo": true + }, + { + "stats": { + "suites": 1, + "tests": 1, + "passes": 1, + "pending": 0, + "skipped": 0, + "failures": 0, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockEndedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234 + }, + "reporter": "spec", + "reporterStats": { + "suites": 1, + "tests": 1, + "passes": 1, + "pending": 0, + "failures": 0, + "start": "2018-02-01T20:14:19.323Z", + "end": "2018-02-01T20:14:19.323Z", + "duration": 1234 + }, + "hooks": [ + { + "hookId": "h1", + "hookName": "before each", + "title": [ + "\"before each\" hook" + ], + "body": "function () {\n return cy.wait(1000);\n }" + } + ], + "tests": [ + { + "testId": "r3", + "title": [ + "simple passing spec", + "passes" + ], + "state": "passed", + "body": "function () {\n return cy.wrap(true).should(\"be.true\");\n }", + "stack": null, + "error": null, + "timings": { + "lifecycle": 100, + "before each": [ + { + "hookId": "h1", + "fnDuration": 400, + "afterFnDuration": 200 + } + ], + "test": { + "fnDuration": 400, + "afterFnDuration": 200 + } + }, + "failedFromHookId": null, + "wallClockStartedAt": "2018-02-01T20:14:19.323Z", + "wallClockDuration": 1234, + "videoTimestamp": 9999 + } + ], + "error": null, + "video": "/foo/bar/.projects/e2e/cypress/videos/abc123.mp4", + "screenshots": [], + "spec": { + "name": "simple_passing_spec.coffee", + "path": "cypress/integration/simple_passing_spec.coffee", + "absolute": "/foo/bar/.projects/e2e/cypress/integration/simple_passing_spec.coffee" + }, + "shouldUploadVideo": true + } + ], + "browserPath": "path/to/browser", + "browserName": "FooBrowser", + "browserVersion": "88", + "osName": "FooOS", + "osVersion": "1234", + "cypressVersion": "9.9.9", + "config": {} +} + diff --git a/packages/server/__snapshots__/specs_spec.coffee b/packages/server/__snapshots__/specs_spec.coffee new file mode 100644 index 000000000000..d61565320ab4 --- /dev/null +++ b/packages/server/__snapshots__/specs_spec.coffee @@ -0,0 +1,14 @@ +exports['e2e specs failing when no specs found 1'] = `Can't run because no spec files were found. + +We searched for any files inside of this folder: + +/foo/bar/.projects/e2e/cypress/specs +` + +exports['e2e specs failing when no spec pattern found 1'] = `Can't run because no spec files were found. + +We searched for any files matching this glob pattern: + +cypress/integration/cypress/integration/**notfound** +` + diff --git a/packages/server/__snapshots__/stdout_spec.coffee b/packages/server/__snapshots__/stdout_spec.coffee index 5d179852e89b..c10195547432 100644 --- a/packages/server/__snapshots__/stdout_spec.coffee +++ b/packages/server/__snapshots__/stdout_spec.coffee @@ -1,7 +1,19 @@ exports['e2e stdout displays errors from failures 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (stdout_failing_spec.coffee) │ + │ Searched: cypress/integration/stdout_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: stdout_failing_spec.coffee... (1 of 1) stdout_failing_spec @@ -82,16 +94,19 @@ The internal Cypress web server responded with: - (Tests Finished) + (Results) - - Tests: 4 - - Passes: 2 - - Failures: 3 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 3 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 5 │ + │ Passing: 2 │ + │ Failing: 3 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 3 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: stdout_failing_spec.coffee │ + └──────────────────────────────────────────┘ (Screenshots) @@ -104,17 +119,39 @@ The internal Cypress web server responded with: (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + +==================================================================================================== - (All Done) + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ stdout_failing_spec.coffee Xs 5 2 3 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 5 2 3 - - ` exports['e2e stdout displays errors from exiting early due to bundle errors 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (stdout_exit_early_failing_spec.coffee) │ + │ Searched: cypress/integration/stdout_exit_early_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: stdout_exit_early_failing_spec.coffee... (1 of 1) - (Tests Starting) Oops...we found an error preparing this test file: /foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee @@ -133,32 +170,56 @@ This occurred while Cypress was compiling and bundling your test code. This is u Fix the error in your code and re-run your tests. - (Tests Finished) + (Results) - - Tests: 0 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────────────────────┐ + │ Tests: 0 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: stdout_exit_early_failing_spec.coffee │ + └─────────────────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ stdout_exit_early_failing_spec.coffee Xs - - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs - - 1 - - ` exports['e2e stdout does not duplicate suites or tests between visits 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (stdout_passing_spec.coffee) │ + │ Searched: cypress/integration/stdout_passing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: stdout_passing_spec.coffee... (1 of 1) stdout_passing_spec @@ -179,25 +240,151 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 8 passing - (Tests Finished) + (Results) - - Tests: 8 - - Passes: 8 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────┐ + │ Tests: 8 │ + │ Passing: 8 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: stdout_passing_spec.coffee │ + └──────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ stdout_passing_spec.coffee Xs 8 8 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 8 8 - - - + +` + +exports['e2e stdout logs that electron cannot be recorded in headed mode 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_spec.coffee) │ + │ Searched: cypress/integration/simple_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_spec.coffee... (1 of 1) + +Warning: Cypress can only record videos when running headlessly. + +You have set the 'electron' browser to run headed. + +A video will not be recorded when using this mode. + + + ✓ is true + + 1 passing + + + (Results) + + ┌──────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: simple_spec.coffee │ + └──────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - + +` + +exports['e2e stdout logs that chrome cannot be recorded 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (simple_spec.coffee) │ + │ Searched: cypress/integration/simple_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: simple_spec.coffee... (1 of 1) + +Warning: Cypress can only record videos when using the built in 'electron' browser. + +You have set the browser to: 'chrome' + +A video will not be recorded when using this browser. + + + ✓ is true + + 1 passing + + + (Results) + + ┌──────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: simple_spec.coffee │ + └──────────────────────────────────┘ + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ simple_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - ` diff --git a/packages/server/__snapshots__/subdomain_spec.coffee b/packages/server/__snapshots__/subdomain_spec.coffee index 78694a874294..428b042ee453 100644 --- a/packages/server/__snapshots__/subdomain_spec.coffee +++ b/packages/server/__snapshots__/subdomain_spec.coffee @@ -1,7 +1,19 @@ exports['e2e subdomain passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (subdomain_spec.coffee) │ + │ Searched: cypress/integration/subdomain_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: subdomain_spec.coffee... (1 of 1) subdomains @@ -20,25 +32,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 2 pending - (Tests Finished) + (Results) - - Tests: 9 - - Passes: 7 - - Failures: 0 - - Pending: 2 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────────┐ + │ Tests: 9 │ + │ Passing: 7 │ + │ Failing: 0 │ + │ Pending: 2 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: subdomain_spec.coffee │ + └─────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ subdomain_spec.coffee Xs 9 7 - 2 - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 9 7 - 2 - ` diff --git a/packages/server/__snapshots__/task_not_registered_spec.coffee b/packages/server/__snapshots__/task_not_registered_spec.coffee new file mode 100644 index 000000000000..a6bbb7315fdf --- /dev/null +++ b/packages/server/__snapshots__/task_not_registered_spec.coffee @@ -0,0 +1,88 @@ +exports['e2e task fails 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (task_not_registered_spec.coffee) │ + │ Searched: cypress/integration/task_not_registered_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: task_not_registered_spec.coffee... (1 of 1) + + + 1) fails because the 'task' event is not registered in plugins file + + 0 passing + 1 failing + + 1) fails because the 'task' event is not registered in plugins file: + CypressError: cy.task('some:task') failed with the following error: + +The 'task' event has not been registered in the plugins file. You must register it before using cy.task() + +Fix this in your plugins file here: +/foo/bar/.projects/task-not-registered/cypress/plugins/index.js + +https://on.cypress.io/api/task + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + + + + + (Results) + + ┌───────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: task_not_registered_spec.coffee │ + └───────────────────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/task-not-registered/cypress/screenshots/fails because the task event is not registered in plugins file.png (1280x720) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/task-not-registered/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ task_not_registered_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - + +` + diff --git a/packages/server/__snapshots__/task_spec.coffee b/packages/server/__snapshots__/task_spec.coffee new file mode 100644 index 000000000000..60f7deb0fb19 --- /dev/null +++ b/packages/server/__snapshots__/task_spec.coffee @@ -0,0 +1,130 @@ +exports['e2e task fails 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (task_spec.coffee) │ + │ Searched: cypress/integration/task_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: task_spec.coffee... (1 of 1) + + + 1) throws when task returns undefined + 2) includes stack trace in error + + 0 passing + 2 failing + + 1) throws when task returns undefined: + CypressError: cy.task('returns:undefined') failed with the following error: + +The task 'returns:undefined' returned undefined. You must return a promise, a value, or null to indicate that the task was handled. + +The task handler was: + +returns:undefined() {} + +Fix this in your plugins file here: +/foo/bar/.projects/e2e/cypress/plugins/index.js + +https://on.cypress.io/api/task + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + + 2) includes stack trace in error: + CypressError: cy.task('errors') failed with the following error: + +> Error: Error thrown in task handler + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + at stack trace line + + + + + (Results) + + ┌────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 0 │ + │ Failing: 2 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 2 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: task_spec.coffee │ + └────────────────────────────────┘ + + + (Screenshots) + + - /foo/bar/.projects/e2e/cypress/screenshots/throws when task returns undefined.png (1280x720) + - /foo/bar/.projects/e2e/cypress/screenshots/includes stack trace in error.png (1280x720) + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ task_spec.coffee Xs 2 - 2 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 - 2 - - + +` + diff --git a/packages/server/__snapshots__/terminal_spec.coffee b/packages/server/__snapshots__/terminal_spec.coffee new file mode 100644 index 000000000000..5e078c893866 --- /dev/null +++ b/packages/server/__snapshots__/terminal_spec.coffee @@ -0,0 +1,44 @@ +exports['lib/util/terminal .table draws a table with 2px margin-left 1'] = `  Spec  Skipped  Pending  Passing  Failing  + ┌────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ foo.js 4 3 2 1 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ bar.js 0 0 0 15 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ fail/is/whale.js 25 5 100 3 │ + └────────────────────────────────────────────────────────────────────────────────────────────────────┘` + +exports['lib/util/terminal .table draws two tables as summary view 1'] = `  Spec  Skipped  Pending  Passing  Failing  + ┌────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ foo.js 4 3 2 1 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ bar.js 0 0 0 15 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ fail/is/whale.js 25 5 100 3 │ + └────────────────────────────────────────────────────────────────────────────────────────────────────┘` + +exports['lib/util/terminal .table draws multiple specs summary table 1'] = ` Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ foo.js 49s 7 4 3 2 1 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ bar.js 6s 0 0 0 0 15 │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ fail/is/whale.js 3m 28s 30 25 5 100 3 │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 2 of 3 passed (66%) 5m 36s 37 29 8 102 18 ` + +exports['lib/util/terminal .table draws single spec summary table 1'] = ` ┌──────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 2 │ + │ Failing: 3 │ + │ Pending: 4 │ + │ Skipped: 5 │ + │ Duration: 6 │ + │ Screenshots: 7 │ + │ Video: true │ + │ Spec: foo/bar/baz.js │ + └──────────────────────────────┘` + +exports['lib/util/terminal .table draws a page divider 1'] = `──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: foo/bar/baz.js... (100 of 200) ` + diff --git a/packages/server/__snapshots__/uncaught_spec_errors_spec.coffee b/packages/server/__snapshots__/uncaught_spec_errors_spec.coffee index 838bc13b0e9c..5fbe51738b03 100644 --- a/packages/server/__snapshots__/uncaught_spec_errors_spec.coffee +++ b/packages/server/__snapshots__/uncaught_spec_errors_spec.coffee @@ -1,7 +1,19 @@ exports['e2e uncaught errors failing1 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (uncaught_synchronous_before_tests_parsed.coffee) │ + │ Searched: cypress/integration/uncaught_synchronous_before_tests_parsed.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: uncaught_synchronous_before_tests_parsed.coffee... (1 of 1) 1) An uncaught error was detected outside of a test @@ -27,16 +39,19 @@ We dynamically generated a new test to display this failure. - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: uncaught_synchronous_before_tests_parsed.coffee │ + └───────────────────────────────────────────────────────────────┘ (Screenshots) @@ -47,17 +62,38 @@ We dynamically generated a new test to display this failure. (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + +==================================================================================================== - (All Done) + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ uncaught_synchronous_before_tests_par… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e uncaught errors failing2 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (uncaught_synchronous_during_hook_spec.coffee) │ + │ Searched: cypress/integration/uncaught_synchronous_during_hook_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: uncaught_synchronous_during_hook_spec.coffee... (1 of 1) 1) An uncaught error was detected outside of a test @@ -84,16 +120,19 @@ We dynamically generated a new test to display this failure. - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: uncaught_synchronous_during_hook_spec.coffee │ + └────────────────────────────────────────────────────────────┘ (Screenshots) @@ -104,17 +143,38 @@ We dynamically generated a new test to display this failure. (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ uncaught_synchronous_during_hook_spec… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e uncaught errors failing3 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (uncaught_during_test_spec.coffee) │ + │ Searched: cypress/integration/uncaught_during_test_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: uncaught_during_test_spec.coffee... (1 of 1) foo @@ -135,16 +195,19 @@ When Cypress detects uncaught errors originating from your test code it will aut - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: uncaught_during_test_spec.coffee │ + └────────────────────────────────────────────────┘ (Screenshots) @@ -155,17 +218,38 @@ When Cypress detects uncaught errors originating from your test code it will aut (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ uncaught_during_test_spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e uncaught errors failing4 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) - (Tests Starting) + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (uncaught_during_hook_spec.coffee) │ + │ Searched: cypress/integration/uncaught_during_hook_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: uncaught_during_hook_spec.coffee... (1 of 1) foo @@ -191,16 +275,19 @@ Because this error occurred during a 'before all' hook we are skipping the remai - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 1 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 1 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: uncaught_during_hook_spec.coffee │ + └────────────────────────────────────────────────┘ (Screenshots) @@ -211,17 +298,38 @@ Because this error occurred during a 'before all' hook we are skipping the remai (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ uncaught_during_hook_spec.coffee Xs 2 1 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 1 1 - - ` exports['e2e uncaught errors failing5 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (caught_async_sync_test_spec.coffee) │ + │ Searched: cypress/integration/caught_async_sync_test_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: caught_async_sync_test_spec.coffee... (1 of 1) foo @@ -257,16 +365,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 - (Tests Finished) + (Results) - - Tests: 8 - - Passes: 4 - - Failures: 4 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 4 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────────────┐ + │ Tests: 8 │ + │ Passing: 4 │ + │ Failing: 4 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 4 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: caught_async_sync_test_spec.coffee │ + └──────────────────────────────────────────────────┘ (Screenshots) @@ -280,10 +391,19 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ caught_async_sync_test_spec.coffee Xs 8 4 4 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 8 4 4 - - ` diff --git a/packages/server/__snapshots__/uncaught_support_file_spec.coffee b/packages/server/__snapshots__/uncaught_support_file_spec.coffee index 58b1b3357521..0163827fea50 100644 --- a/packages/server/__snapshots__/uncaught_support_file_spec.coffee +++ b/packages/server/__snapshots__/uncaught_support_file_spec.coffee @@ -1,7 +1,18 @@ exports['e2e uncaught support file errors failing 1'] = ` -Started video recording: /foo/bar/.projects/uncaught-support-file/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (spec.coffee) │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: spec.coffee... (1 of 1) 1) An uncaught error was detected outside of a test @@ -27,16 +38,19 @@ We dynamically generated a new test to display this failure. - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: spec.coffee │ + └───────────────────────────┘ (Screenshots) @@ -47,10 +61,19 @@ We dynamically generated a new test to display this failure. (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/uncaught-support-file/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/uncaught-support-file/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ spec.coffee Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` diff --git a/packages/server/__snapshots__/user_agent_spec.coffee b/packages/server/__snapshots__/user_agent_spec.coffee index 5b6cdd118fff..f352eba47c38 100644 --- a/packages/server/__snapshots__/user_agent_spec.coffee +++ b/packages/server/__snapshots__/user_agent_spec.coffee @@ -1,9 +1,25 @@ -exports['e2e user agent passes on chrome 1'] = `Warning: Cypress can only record videos when using the built in 'electron' browser. +exports['e2e user agent passes on chrome 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (user_agent_spec.coffee) │ + │ Searched: cypress/integration/user_agent_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: user_agent_spec.coffee... (1 of 1) + +Warning: Cypress can only record videos when using the built in 'electron' browser. You have set the browser to: 'chrome' A video will not be recorded when using this browser. - (Tests Starting) user agent @@ -14,26 +30,50 @@ A video will not be recorded when using this browser. 2 passing - (Tests Finished) + (Results) + + ┌──────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: false │ + │ Duration: X seconds │ + │ Spec Ran: user_agent_spec.coffee │ + └──────────────────────────────────────┘ + - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: false - - Cypress Version: 1.2.3 +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ user_agent_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` exports['e2e user agent passes on electron 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (user_agent_spec.coffee) │ + │ Searched: cypress/integration/user_agent_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: user_agent_spec.coffee... (1 of 1) user agent @@ -44,25 +84,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 2 passing - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 2 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 2 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: user_agent_spec.coffee │ + └──────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ user_agent_spec.coffee Xs 2 2 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 2 2 - - - ` diff --git a/packages/server/__snapshots__/viewport_spec.coffee b/packages/server/__snapshots__/viewport_spec.coffee index 9ab7c0423cdc..083e34d9a1f3 100644 --- a/packages/server/__snapshots__/viewport_spec.coffee +++ b/packages/server/__snapshots__/viewport_spec.coffee @@ -1,7 +1,19 @@ exports['e2e viewport passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (viewport_spec.coffee) │ + │ Searched: cypress/integration/viewport_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: viewport_spec.coffee... (1 of 1) viewport @@ -13,25 +25,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 3 passing - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 3 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 3 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: viewport_spec.coffee │ + └────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ viewport_spec.coffee Xs 3 3 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 3 3 - - - ` diff --git a/packages/server/__snapshots__/visit_spec.coffee b/packages/server/__snapshots__/visit_spec.coffee index 50c9fbc85c3b..d4d1adcf3dab 100644 --- a/packages/server/__snapshots__/visit_spec.coffee +++ b/packages/server/__snapshots__/visit_spec.coffee @@ -1,7 +1,19 @@ exports['e2e visit low response timeout passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (visit_spec.coffee) │ + │ Searched: cypress/integration/visit_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: visit_spec.coffee... (1 of 1) visits @@ -28,32 +40,56 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 13 passing - (Tests Finished) + (Results) - - Tests: 13 - - Passes: 13 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌─────────────────────────────────┐ + │ Tests: 13 │ + │ Passing: 13 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: visit_spec.coffee │ + └─────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ visit_spec.coffee Xs 13 13 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 13 13 - - - ` exports['e2e visit low response timeout fails when network connection immediately fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (visit_http_network_error_failing_spec.coffee) │ + │ Searched: cypress/integration/visit_http_network_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: visit_http_network_error_failing_spec.coffee... (1 of 1) when network connection cannot be established @@ -106,16 +142,19 @@ Error: connect ECONNREFUSED 127.0.0.1:16795 - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: visit_http_network_error_failing_spec.coffee │ + └────────────────────────────────────────────────────────────┘ (Screenshots) @@ -126,17 +165,38 @@ Error: connect ECONNREFUSED 127.0.0.1:16795 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ visit_http_network_error_failing_spec… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e visit low response timeout fails when server responds with 500 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (visit_http_500_response_failing_spec.coffee) │ + │ Searched: cypress/integration/visit_http_500_response_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: visit_http_500_response_failing_spec.coffee... (1 of 1) when server response is 500 @@ -177,16 +237,19 @@ If you do not want status codes to cause failures pass the option: 'failOnStatus - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: visit_http_500_response_failing_spec.coffee │ + └───────────────────────────────────────────────────────────┘ (Screenshots) @@ -197,17 +260,38 @@ If you do not want status codes to cause failures pass the option: 'failOnStatus (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ visit_http_500_response_failing_spec.… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e visit low response timeout fails when file server responds with 404 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (visit_file_404_response_failing_spec.coffee) │ + │ Searched: cypress/integration/visit_file_404_response_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: visit_file_404_response_failing_spec.coffee... (1 of 1) when file server response is 404 @@ -248,16 +332,19 @@ The internal Cypress web server responded with: - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: visit_file_404_response_failing_spec.coffee │ + └───────────────────────────────────────────────────────────┘ (Screenshots) @@ -268,17 +355,38 @@ The internal Cypress web server responded with: (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + (Run Finished) - (All Done) + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ visit_file_404_response_failing_spec.… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e visit low response timeout fails when content type isnt html 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (visit_non_html_content_type_failing_spec.coffee) │ + │ Searched: cypress/integration/visit_non_html_content_type_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + - (Tests Starting) +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: visit_non_html_content_type_failing_spec.coffee... (1 of 1) when content type is plain/text @@ -321,16 +429,19 @@ cy.request() will automatically get and set cookies and enable you to parse resp - (Tests Finished) + (Results) - - Tests: 1 - - Passes: 0 - - Failures: 1 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 1 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 0 │ + │ Failing: 1 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 1 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: visit_non_html_content_type_failing_spec.coffee │ + └───────────────────────────────────────────────────────────────┘ (Screenshots) @@ -341,17 +452,38 @@ cy.request() will automatically get and set cookies and enable you to parse resp (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ visit_non_html_content_type_failing_s… Xs 1 - 1 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 1 - 1 - - ` exports['e2e visit normal response timeouts fails when visit times out 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (visit_http_timeout_failing_spec.coffee) │ + │ Searched: cypress/integration/visit_http_timeout_failing_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ - (Tests Starting) + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: visit_http_timeout_failing_spec.coffee... (1 of 1) when visit times out @@ -419,16 +551,19 @@ When this 'load' event occurs, Cypress will continue running commands. - (Tests Finished) + (Results) - - Tests: 2 - - Passes: 0 - - Failures: 2 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 2 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌──────────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 0 │ + │ Failing: 2 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 2 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: visit_http_timeout_failing_spec.coffee │ + └──────────────────────────────────────────────────────┘ (Screenshots) @@ -440,10 +575,19 @@ When this 'load' event occurs, Cypress will continue running commands. (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ visit_http_timeout_failing_spec.coffee Xs 2 - 2 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 2 - 2 - - ` diff --git a/packages/server/__snapshots__/web_security_spec.coffee b/packages/server/__snapshots__/web_security_spec.coffee index a7d5cec0a741..23acffc4b104 100644 --- a/packages/server/__snapshots__/web_security_spec.coffee +++ b/packages/server/__snapshots__/web_security_spec.coffee @@ -1,7 +1,19 @@ exports['e2e web security when enabled fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (web_security_spec.coffee) │ + │ Searched: cypress/integration/web_security_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: web_security_spec.coffee... (1 of 1) web security @@ -112,16 +124,19 @@ https://on.cypress.io/cross-origin-violation - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 0 - - Failures: 3 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 3 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 0 │ + │ Failing: 3 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 3 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: web_security_spec.coffee │ + └────────────────────────────────────────┘ (Screenshots) @@ -134,17 +149,38 @@ https://on.cypress.io/cross-origin-violation (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ web_security_spec.coffee Xs 3 - 3 - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + 1 of 1 failed (100%) Xs 3 - 3 - - ` exports['e2e web security when disabled fails 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (web_security_spec.coffee) │ + │ Searched: cypress/integration/web_security_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: web_security_spec.coffee... (1 of 1) web security @@ -156,25 +192,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 3 passing - (Tests Finished) + (Results) - - Tests: 3 - - Passes: 3 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌────────────────────────────────────────┐ + │ Tests: 3 │ + │ Passing: 3 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: web_security_spec.coffee │ + └────────────────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ web_security_spec.coffee Xs 3 3 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 3 3 - - - ` diff --git a/packages/server/__snapshots__/websockets_spec.coffee b/packages/server/__snapshots__/websockets_spec.coffee new file mode 100644 index 000000000000..5e872ad1b246 --- /dev/null +++ b/packages/server/__snapshots__/websockets_spec.coffee @@ -0,0 +1,59 @@ +exports['e2e websockets passes 1'] = ` +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (websockets_spec.coffee) │ + │ Searched: cypress/integration/websockets_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: websockets_spec.coffee... (1 of 1) + + + websockets + ✓ does not crash + + + 1 passing + + + (Results) + + ┌──────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: websockets_spec.coffee │ + └──────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ websockets_spec.coffee Xs 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 1 1 - - - + +` + diff --git a/packages/server/__snapshots__/xhr_spec.coffee b/packages/server/__snapshots__/xhr_spec.coffee index 1f8d21df72fd..06032bc2d82b 100644 --- a/packages/server/__snapshots__/xhr_spec.coffee +++ b/packages/server/__snapshots__/xhr_spec.coffee @@ -1,7 +1,19 @@ exports['e2e xhr passes 1'] = ` -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 1 found (xhr_spec.coffee) │ + │ Searched: cypress/integration/xhr_spec.coffee │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: xhr_spec.coffee... (1 of 1) xhrs @@ -19,25 +31,37 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 8 passing - (Tests Finished) + (Results) - - Tests: 8 - - Passes: 8 - - Failures: 0 - - Pending: 0 - - Duration: 10 seconds - - Screenshots: 0 - - Video Recorded: true - - Cypress Version: 1.2.3 + ┌───────────────────────────────┐ + │ Tests: 8 │ + │ Passing: 8 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: xhr_spec.coffee │ + └───────────────────────────────┘ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) + + +==================================================================================================== + + (Run Finished) - (All Done) + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✔ xhr_spec.coffee Xs 8 8 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + All specs passed! Xs 8 8 - - - ` diff --git a/packages/server/lib/api.coffee b/packages/server/lib/api.coffee index 4b77ce5b143f..f73d0dc5b82b 100644 --- a/packages/server/lib/api.coffee +++ b/packages/server/lib/api.coffee @@ -6,15 +6,24 @@ errors = require("request-promise/errors") Promise = require("bluebird") pkg = require("@packages/root") browsers = require('./browsers') -Routes = require("./util/routes") +routes = require("./util/routes") system = require("./util/system") debug = require("debug")("cypress:server:api") +## TODO: improve this, dont just use +## requests because its way too verbose +# if debug.enabled +# request.debug = true + rp = request.defaults (params = {}, callback) -> + _.defaults(params, { + gzip: true + }) + headers = params.headers ?= {} _.defaults(headers, { - "x-platform": os.platform() + "x-os-name": os.platform() "x-cypress-version": pkg.version }) @@ -43,12 +52,12 @@ machineId = -> module.exports = { ping: -> - rp.get(Routes.ping()) + rp.get(routes.ping()) .catch(tagError) getOrgs: (authToken) -> rp.get({ - url: Routes.orgs() + url: routes.orgs() json: true auth: { bearer: authToken @@ -58,7 +67,7 @@ module.exports = { getProjects: (authToken) -> rp.get({ - url: Routes.projects() + url: routes.projects() json: true auth: { bearer: authToken @@ -68,7 +77,7 @@ module.exports = { getProject: (projectId, authToken) -> rp.get({ - url: Routes.project(projectId) + url: routes.project(projectId) json: true auth: { bearer: authToken @@ -81,90 +90,71 @@ module.exports = { getProjectRuns: (projectId, authToken, options = {}) -> options.page ?= 1 + rp.get({ - url: Routes.projectRuns(projectId) + url: routes.projectRuns(projectId) json: true timeout: options.timeout ? 10000 auth: { bearer: authToken } + headers: { + "x-route-version": "2" + } }) .catch(errors.StatusCodeError, formatResponseBody) .catch(tagError) createRun: (options = {}) -> - debugReturnedBuild = (info) -> - debug("received API response with buildId %s", info.buildId) - debug("and list of specs to run", info.specs) - body = _.pick(options, [ "projectId" "recordKey" - "commitSha" - "commitBranch" - "commitAuthorName" - "commitAuthorEmail" - "commitMessage" - "remoteOrigin" - "ciParams" - "ciProvider" - "ciBuildNumber", - "groupId", + "ci" "specs", + "commit" + "platform" "specPattern" ]) - debug("creating project run") - debug("project '%s' group id '%s'", body.projectId, body.groupId) - rp.post({ - url: Routes.runs() + body + url: routes.runs() json: true timeout: options.timeout ? 10000 headers: { - "x-route-version": "2" + "x-route-version": "3" } - body: body }) - .promise() - .tap(debugReturnedBuild) - .get("buildId") .catch(errors.StatusCodeError, formatResponseBody) .catch(tagError) createInstance: (options = {}) -> - { buildId, spec, timeout } = options - - browsers.getByName(options.browser) - .then (browser = {}) -> - ## get the formatted browserName - ## and version of the browser we're - ## about to be running on - { displayName, version } = browser - - system.info() - .then (systemInfo) -> - systemInfo.spec = spec - systemInfo.browserName = displayName - systemInfo.browserVersion = version - - rp.post({ - url: Routes.instances(buildId) - json: true - timeout: timeout ? 10000 - headers: { - "x-route-version": "3" - } - body: systemInfo - }) - .promise() - .get("instanceId") - .catch(errors.StatusCodeError, formatResponseBody) - .catch(tagError) + { runId, timeout } = options + + body = _.pick(options, [ + "spec" + "planId" + "machineId" + "platform" + ]) + + rp.post({ + body + url: routes.instances(runId) + json: true + timeout: timeout ? 10000 + headers: { + "x-route-version": "4" + } + }) + .promise() + .get("instanceId") + .catch(errors.StatusCodeError, formatResponseBody) + .catch(tagError) updateInstanceStdout: (options = {}) -> rp.put({ - url: Routes.instanceStdout(options.instanceId) + url: routes.instanceStdout(options.instanceId) json: true timeout: options.timeout ? 10000 body: { @@ -176,22 +166,22 @@ module.exports = { updateInstance: (options = {}) -> rp.put({ - url: Routes.instance(options.instanceId) + url: routes.instance(options.instanceId) json: true timeout: options.timeout ? 10000 + headers: { + "x-route-version": "2" + } body: _.pick(options, [ + "stats" "tests" - "duration" - "passes" - "failures" - "pending" "error" "video" + "hooks" + "stdout" "screenshots" - "failingTests" - "ciProvider" ## TODO: don't send this (no reason to) "cypressConfig" - "stdout" + "reporterStats" ]) }) .catch(errors.StatusCodeError, formatResponseBody) @@ -199,7 +189,7 @@ module.exports = { createRaygunException: (body, authToken, timeout = 3000) -> rp.post({ - url: Routes.exceptions() + url: routes.exceptions() json: true body: body auth: { @@ -222,7 +212,7 @@ module.exports = { h["x-machine-id"] = id rp.post({ - url: Routes.signin({code: code}) + url: routes.signin({code: code}) json: true headers: h }) @@ -237,7 +227,7 @@ module.exports = { createSignout: (authToken) -> rp.post({ - url: Routes.signout() + url: routes.signout() json: true auth: { bearer: authToken @@ -248,7 +238,7 @@ module.exports = { createProject: (projectDetails, remoteOrigin, authToken) -> rp.post({ - url: Routes.projects() + url: routes.projects() json: true auth: { bearer: authToken @@ -268,7 +258,7 @@ module.exports = { getProjectRecordKeys: (projectId, authToken) -> rp.get({ - url: Routes.projectRecordKeys(projectId) + url: routes.projectRecordKeys(projectId) json: true auth: { bearer: authToken @@ -278,7 +268,7 @@ module.exports = { requestAccess: (projectId, authToken) -> rp.post({ - url: Routes.membershipRequests(projectId) + url: routes.membershipRequests(projectId) json: true auth: { bearer: authToken @@ -289,7 +279,7 @@ module.exports = { getLoginUrl: -> rp.get({ - url: Routes.auth(), + url: routes.auth(), json: true }) .promise() @@ -299,7 +289,7 @@ module.exports = { _projectToken: (method, projectId, authToken) -> rp({ method: method - url: Routes.projectToken(projectId) + url: routes.projectToken(projectId) json: true auth: { bearer: authToken diff --git a/packages/server/lib/automation/screenshot.coffee b/packages/server/lib/automation/screenshot.coffee index 31928d2662c8..1e5fc565a45c 100644 --- a/packages/server/lib/automation/screenshot.coffee +++ b/packages/server/lib/automation/screenshot.coffee @@ -1,9 +1,17 @@ +log = require("debug")("cypress:server:screenshot") screenshots = require("../screenshots") module.exports = (screenshotsFolder) -> return { capture: (data, automate) -> - automate(data) - .then (dataUrl) -> - screenshots.save(data, dataUrl, screenshotsFolder) + log("capture %o", data) + + screenshots.capture(data, automate) + .then (details) -> + if details + screenshots.save(data, details, screenshotsFolder) + .catch (err) -> + screenshots.clearMultipartState() + throw err + } diff --git a/packages/server/lib/browsers/chrome.coffee b/packages/server/lib/browsers/chrome.coffee index 34a3d4273640..20947ae8c8ea 100644 --- a/packages/server/lib/browsers/chrome.coffee +++ b/packages/server/lib/browsers/chrome.coffee @@ -1,15 +1,13 @@ _ = require("lodash") os = require("os") -fs = require("fs-extra") Promise = require("bluebird") extension = require("@packages/extension") debug = require("debug")("cypress:server:browsers") plugins = require("../plugins") +fs = require("../util/fs") appData = require("../util/app_data") utils = require("./utils") -fs = Promise.promisifyAll(fs) - LOAD_EXTENSION = "--load-extension=" pathToExtension = extension.getPathToExtension() @@ -40,6 +38,8 @@ defaultArgs = [ "--reduce-security-for-testing" "--enable-automation" "--disable-infobars" + "--disable-device-discovery-notifications" + "--disable-blink-features=RootLayerScrolling" ## the following come frome chromedriver ## https://code.google.com/p/chromium/codesearch#chromium/src/chrome/test/chromedriver/chrome_launcher.cc&sq=package:chromium&l=70 diff --git a/packages/server/lib/browsers/electron.coffee b/packages/server/lib/browsers/electron.coffee index e0c6fd6794d6..5b0741d38c28 100644 --- a/packages/server/lib/browsers/electron.coffee +++ b/packages/server/lib/browsers/electron.coffee @@ -8,7 +8,7 @@ Windows = require("../gui/windows") savedState = require("../saved_state") module.exports = { - _defaultOptions: (projectPath, state, options) -> + _defaultOptions: (projectRoot, state, options) -> _this = @ defaults = { @@ -32,7 +32,7 @@ module.exports = { onNewWindow: (e, url) -> _win = @ - _this._launchChild(e, url, _win, projectPath, state, options) + _this._launchChild(e, url, _win, projectRoot, state, options) .then (child) -> ## close child on parent close _win.on "close", -> @@ -42,17 +42,17 @@ module.exports = { _.defaultsDeep({}, options, defaults) - _render: (url, projectPath, options = {}) -> - win = Windows.create(projectPath, options) + _render: (url, projectRoot, options = {}) -> + win = Windows.create(projectRoot, options) @_launch(win, url, options) - _launchChild: (e, url, parent, projectPath, state, options) -> + _launchChild: (e, url, parent, projectRoot, state, options) -> e.preventDefault() [parentX, parentY] = parent.getPosition() - options = @_defaultOptions(projectPath, state, options) + options = @_defaultOptions(projectRoot, state, options) _.extend(options, { x: parentX + 100 @@ -61,7 +61,7 @@ module.exports = { onPaint: null ## dont capture paint events }) - win = Windows.create(projectPath, options) + win = Windows.create(projectRoot, options) ## needed by electron since we prevented default and are creating ## our own BrowserWindow (https://electron.atom.io/docs/api/web-contents/#event-new-window) @@ -107,14 +107,14 @@ module.exports = { }, resolve) open: (browserName, url, options = {}, automation) -> - { projectPath } = options + { projectRoot } = options - savedState(projectPath) + savedState(projectRoot) .then (state) -> state.get() .then (state) => ## get our electron default options - options = @_defaultOptions(projectPath, state, options) + options = @_defaultOptions(projectRoot, state, options) ## get the GUI window defaults now options = Windows.defaults(options) @@ -128,7 +128,7 @@ module.exports = { .then (newOptions) -> return newOptions ? options .then (options) => - @_render(url, projectPath, options) + @_render(url, projectRoot, options) .then (win) => a = Windows.automation(win) diff --git a/packages/server/lib/browsers/index.coffee b/packages/server/lib/browsers/index.coffee index 919811732245..786ceb3661af 100644 --- a/packages/server/lib/browsers/index.coffee +++ b/packages/server/lib/browsers/index.coffee @@ -1,13 +1,12 @@ _ = require("lodash") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") debug = require("debug")("cypress:server:browsers") utils = require("./utils") errors = require("../errors") +fs = require("../util/fs") -fs = Promise.promisifyAll(fs) -instance = null +instance = null kill = (unbind) -> ## cleanup our running browser @@ -42,24 +41,30 @@ getBrowser = (name) -> find = (browser, browsers = []) -> _.find(browsers, { name: browser }) -getByName = (browser) -> +ensureAndGetByName = (name) -> utils.getBrowsers() .then (browsers = []) -> - find(browser, browsers) + find(name, browsers) or throwBrowserNotFound(name, browsers) + +throwBrowserNotFound = (browser, browsers = []) -> + names = _.map(browsers, "name").join(", ") + errors.throw("BROWSER_NOT_FOUND", browser, names) process.once "exit", kill module.exports = { + find + + ensureAndGetByName + + throwBrowserNotFound + get: utils.getBrowsers launch: utils.launch close: kill - find - - getByName - open: (name, options = {}, automation) -> kill(true) .then -> @@ -69,8 +74,7 @@ module.exports = { }) if not browser = getBrowser(name) - names = _.map(options.browsers, "name").join(", ") - return errors.throw("BROWSER_NOT_FOUND", name, names) + return throwBrowserNotFound(name, options.browsers) ## set the current browser object on options ## so we can pass it down @@ -80,6 +84,7 @@ module.exports = { throw new Error("options.url must be provided when opening a browser. You passed:", options) debug("opening browser %s", name) + browser.open(name, url, options, automation) .then (i) -> debug("browser opened") diff --git a/packages/server/lib/browsers/utils.coffee b/packages/server/lib/browsers/utils.coffee index 392ca5cf6ca4..45bd959c6b67 100644 --- a/packages/server/lib/browsers/utils.coffee +++ b/packages/server/lib/browsers/utils.coffee @@ -1,11 +1,9 @@ -fs = require("fs-extra") path = require("path") Promise = require("bluebird") launcher = require("@packages/launcher") +fs = require("../util/fs") appData = require("../util/app_data") -fs = Promise.promisifyAll(fs) - profiles = appData.path("browsers") module.exports = { diff --git a/packages/server/lib/cache.coffee b/packages/server/lib/cache.coffee index 710aec6a2c55..9832c767e850 100644 --- a/packages/server/lib/cache.coffee +++ b/packages/server/lib/cache.coffee @@ -1,13 +1,11 @@ _ = require("lodash") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") +fs = require("./util/fs") appData = require("./util/app_data") FileUtil = require("./util/file") logger = require("./logger") -fs = Promise.promisifyAll(fs) - fileUtil = new FileUtil({ path: appData.path("cache") }) @@ -61,7 +59,7 @@ module.exports = { tx.set({PROJECTS: projects}) - getProjectPaths: -> + getProjectRoots: -> fileUtil.transaction (tx) => @_getProjects(tx).then (projects) => pathsToRemove = Promise.reduce projects, (memo, path) -> diff --git a/packages/server/lib/capture.coffee b/packages/server/lib/capture.coffee new file mode 100644 index 000000000000..131dfadc5706 --- /dev/null +++ b/packages/server/lib/capture.coffee @@ -0,0 +1,46 @@ +_write = process.stdout.write +_log = process.log + +restore = -> + ## restore to the originals + process.stdout.write = _write + process.log = _log + +stdout = -> + ## always restore right when we start capturing + # restore() + + logs = [] + + ## lazily backup write to enable + ## injection + write = process.stdout.write + log = process.log + + ## electron adds a new process.log + ## method for windows instead of process.stdout.write + ## https://github.com/cypress-io/cypress/issues/977 + if log + process.log = (str) -> + logs.push(str) + + log.apply(@, arguments) + + process.stdout.write = (str) -> + logs.push(str) + + write.apply(@, arguments) + + return { + toString: -> logs.join("") + + data: logs + + restore + } + +module.exports = { + stdout + + restore +} diff --git a/packages/server/lib/config.coffee b/packages/server/lib/config.coffee index 2bb2a9cd69d0..687da04473c0 100644 --- a/packages/server/lib/config.coffee +++ b/packages/server/lib/config.coffee @@ -1,11 +1,11 @@ _ = require("lodash") path = require("path") Promise = require("bluebird") -fs = require("fs-extra") deepDiff = require("return-deep-diff") errors = require("./errors") scaffold = require("./scaffold") errors = require("./errors") +fs = require("./util/fs") origin = require("./util/origin") coerce = require("./util/coerce") settings = require("./util/settings") @@ -41,13 +41,12 @@ configKeys = toWords """ port supportFolder reporter videosFolder reporterOptions - screenshotOnHeadlessFailure defaultCommandTimeout - testFiles execTimeout - trashAssetsBeforeHeadlessRuns pageLoadTimeout - blacklistHosts requestTimeout - userAgent responseTimeout - viewportWidth - viewportHeight + testFiles defaultCommandTimeout + trashAssetsBeforeHeadlessRuns execTimeout + blacklistHosts pageLoadTimeout + userAgent requestTimeout + viewportWidth responseTimeout + viewportHeight taskTimeout videoRecording videoCompression videoUploadOnPasses @@ -99,6 +98,7 @@ defaults = { responseTimeout: 30000 pageLoadTimeout: 60000 execTimeout: 60000 + taskTimeout: 60000 videoRecording: true videoCompression: 32 videoUploadOnPasses: true @@ -108,7 +108,6 @@ defaults = { animationDistanceThreshold: 5 numTestsKeptInMemory: 50 watchForFileChanges: true - screenshotOnHeadlessFailure: true trashAssetsBeforeHeadlessRuns: true autoOpen: false viewportWidth: 1000 @@ -147,8 +146,8 @@ validationRules = { requestTimeout: v.isNumber responseTimeout: v.isNumber testFiles: v.isString - screenshotOnHeadlessFailure: v.isBoolean supportFile: v.isStringOrFalse + taskTimeout: v.isNumber trashAssetsBeforeHeadlessRuns: v.isBoolean userAgent: v.isString videoCompression: v.isNumberOrFalse @@ -327,10 +326,8 @@ module.exports = { setScaffoldPaths: (obj) -> obj = _.clone(obj) - fileName = scaffold.integrationExampleName() - - obj.integrationExampleFile = path.join(obj.integrationFolder, fileName) - obj.integrationExampleName = fileName + obj.integrationExampleName = scaffold.integrationExampleName() + obj.integrationExamplePath = path.join(obj.integrationFolder, obj.integrationExampleName) obj.scaffoldedFiles = scaffold.fileTree(obj) return obj diff --git a/packages/server/lib/controllers/files.coffee b/packages/server/lib/controllers/files.coffee index 9e0182b7bda8..1774881b262e 100644 --- a/packages/server/lib/controllers/files.coffee +++ b/packages/server/lib/controllers/files.coffee @@ -1,23 +1,19 @@ _ = require("lodash") path = require("path") -glob = require("glob") Promise = require("bluebird") -minimatch = require("minimatch") cwd = require("../cwd") -api = require("../api") -user = require("../user") +glob = require("../util/glob") +specsUtil = require("../util/specs") pathHelpers = require("../util/path_helpers") CacheBuster = require("../util/cache_buster") -errors = require("../errors") -log = require("debug")("cypress:server:files") - -glob = Promise.promisify(glob) module.exports = { handleFiles: (req, res, config) -> - @getTestFiles(config) + specsUtil.find(config) .then (files) -> - res.json files + res.json({ + integration: files + }) handleIframe: (req, res, config, getRemoteState) -> test = req.params[0] @@ -47,8 +43,7 @@ module.exports = { getSpecs = => ## grab all of the specs if this is ci if spec is "__all" - @getTestFiles(config) - .get("integration") + specsUtil.find(config) .map (spec) -> ## grab the name of each spec.absolute @@ -101,98 +96,4 @@ module.exports = { .map (filePath) => @prepareForBrowser(filePath, projectRoot) - getTestFiles: (config, specPattern) -> - integrationFolderPath = config.integrationFolder - log("looking for test files in the integration folder %s", - integrationFolderPath) - - log("specPattern for test files is", specPattern) - - ## support files are not automatically - ## ignored because only _fixtures are hard - ## coded. the rest is simply whatever is in - ## the javascripts array - - if config.fixturesFolder - fixturesFolderPath = path.join( - config.fixturesFolder, - "**", - "*" - ) - - supportFilePath = config.supportFile or [] - - ## map all of the javascripts to the project root - ## TODO: think about moving this into config - ## and mapping each of the javascripts into an - ## absolute path - javascriptsPaths = _.map config.javascripts, (js) -> - path.join(config.projectRoot, js) - - ## ignore fixtures + javascripts - options = { - sort: true - absolute: true - cwd: integrationFolderPath - ignore: _.compact(_.flatten([ - javascriptsPaths - supportFilePath - fixturesFolderPath - ])) - } - - ## filePath = /Users/bmann/Dev/my-project/cypress/integration/foo.coffee - ## integrationFolderPath = /Users/bmann/Dev/my-project/cypress/integration - ## relativePathFromIntegrationFolder = foo.coffee - ## relativePathFromProjectRoot = cypress/integration/foo.coffee - - relativePathFromIntegrationFolder = (file) -> - path.relative(integrationFolderPath, file) - - relativePathFromProjectRoot = (file) -> - path.relative(config.projectRoot, file) - - setNameParts = (file) -> - log("found test file %s", file) - throw new Error("Cannot set parts of file from non-absolute path #{file}") if not path.isAbsolute(file) - - { - name: relativePathFromIntegrationFolder(file) - path: relativePathFromProjectRoot(file) - absolute: file - } - - ignorePatterns = [].concat(config.ignoreTestFiles) - - ## a function which returns true if the file does NOT match - ## all of our ignored patterns - doesNotMatchAllIgnoredPatterns = (file) -> - ## using {dot: true} here so that folders with a '.' in them are matched - ## as regular characters without needing an '.' in the - ## using {matchBase: true} here so that patterns without a globstar ** - ## match against the basename of the file - _.every ignorePatterns, (pattern) -> - not minimatch(file, pattern, {dot: true, matchBase: true}) - - matchesSpecPattern = (file) -> - if not specPattern - return true - - minimatch(file, specPattern, { dot: true, matchBase: true }) - - ## grab all the files - glob(config.testFiles, options) - - - ## filter out anything that matches our - ## ignored test files glob - .filter(doesNotMatchAllIgnoredPatterns) - .filter(matchesSpecPattern) - .map(setNameParts) - .then (files) -> - log("found %d spec files", files.length) - log(files) - { - integration: files - } } diff --git a/packages/server/lib/controllers/spec.coffee b/packages/server/lib/controllers/spec.coffee index 661898a5a3dd..ca84b99fff79 100644 --- a/packages/server/lib/controllers/spec.coffee +++ b/packages/server/lib/controllers/spec.coffee @@ -34,6 +34,7 @@ module.exports = { err = errors.get("BUNDLE_ERROR", filePath, preprocessor.errorMessage(err)) + console.log("") errors.log(err) project.emit("exitEarlyWithErr", err.message) diff --git a/packages/server/lib/cypress.coffee b/packages/server/lib/cypress.coffee index 0af7c8240efb..0c107bb50736 100644 --- a/packages/server/lib/cypress.coffee +++ b/packages/server/lib/cypress.coffee @@ -13,13 +13,13 @@ _ = require("lodash") cp = require("child_process") path = require("path") Promise = require("bluebird") -log = require('./log') +debug = require('debug')('cypress:server:cypress') exit = (code = 0) -> ## TODO: we shouldn't have to do this ## but cannot figure out how null is ## being passed into exit - log("about to exit with code", code) + debug("about to exit with code", code) process.exit(code) exit0 = -> @@ -29,7 +29,8 @@ exitErr = (err) -> ## log errors to the console ## and potentially raygun ## and exit with 1 - log('exiting with err', err) + debug('exiting with err', err) + require("./errors").log(err) .then -> exit(1) @@ -54,10 +55,10 @@ module.exports = { new Promise (resolve) -> cypressElectron = require("@packages/electron") fn = (code) -> - ## juggle up the failures since our outer + ## juggle up the totalFailed since our outer ## promise is expecting this object structure - log("electron finished with", code) - resolve({failures: code}) + debug("electron finished with", code) + resolve({totalFailed: code}) cypressElectron.open(".", require("./util/args").toArray(options), fn) openProject: (options) -> @@ -107,8 +108,7 @@ module.exports = { # require("opn")("http://127.0.0.1:8080/debug?ws=127.0.0.1:8080&port=5858") start: (argv = []) -> - require("./logger").info("starting desktop app", args: argv) - log("starting cypress server") + debug("starting cypress with argv %o", argv) ## make sure we have the appData folder require("./util/app_data").ensure() @@ -119,9 +119,6 @@ module.exports = { ## the passed in arguments / options ## and normalize this mode switch - when options.removeIds - options.mode = "removeIds" - when options.version options.mode = "version" @@ -146,39 +143,25 @@ module.exports = { when options.exitWithCode? options.mode = "exitWithCode" - ## enable old CLI tools to record - when options.record or options.ci - options.mode = "record" - when options.runProject - ## go into headless mode when told to run - options.mode = "headless" + ## go into headless mode when running + ## until completion + exit + options.mode = "run" else - ## set the default mode as headed - options.mode ?= "headed" + ## set the default mode as interactive + options.mode ?= "interactive" ## remove mode from options mode = options.mode options = _.omit(options, "mode") - ## TODO: temporary hack to get this commit in - ## before spec parallelization lands - if _.isArray(options.spec) - options.spec = options.spec[0] - @startInMode(mode, options) startInMode: (mode, options) -> - log("start in mode %s with options %j", mode, options) - switch mode - when "removeIds" - require("./project").removeIds(options.projectPath) - .then (stats = {}) -> - console.log("Removed '#{stats.ids}' ids from '#{stats.files}' files.") - .then(exit0) - .catch(exitErr) + debug("start in mode %s with options %j", mode, options) + switch mode when "version" require("./modes/pkg")(options) .get("version") @@ -215,7 +198,7 @@ module.exports = { when "getKey" ## print the key + exit - require("./project").getSecretKeyByPath(options.projectPath) + require("./project").getSecretKeyByPath(options.projectRoot) .then (key) -> console.log(key) .then(exit0) @@ -223,7 +206,7 @@ module.exports = { when "generateKey" ## generate + print the key + exit - require("./project").generateSecretKeyByPath(options.projectPath) + require("./project").generateSecretKeyByPath(options.projectRoot) .then (key) -> console.log(key) .then(exit0) @@ -234,22 +217,16 @@ module.exports = { .then(exit) .catch(exitErr) - when "headless" + when "run" ## run headlessly and exit + ## with num of totalFailed @runElectron(mode, options) - .get("failures") + .get("totalFailed") .then(exit) .catch(exitErr) - when "headed" - @runElectron(mode, options) - - when "record" - ## run headlessly, record, and exit + when "interactive" @runElectron(mode, options) - .get("failures") - .then(exit) - .catch(exitErr) when "server" @runServer(options) diff --git a/packages/server/lib/environment.coffee b/packages/server/lib/environment.coffee index d72eb759ebcb..ede75db34566 100644 --- a/packages/server/lib/environment.coffee +++ b/packages/server/lib/environment.coffee @@ -1,5 +1,5 @@ require("./util/http_overrides") -require("./fs_warn")(require("fs-extra")) +require("./util/fs") os = require("os") cwd = require("./cwd") diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index 92cdc4965d3d..bb58491b3a4c 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -11,7 +11,7 @@ listPaths = (paths) -> API = { # forms well-formatted user-friendly error for most common # errors Cypress can encounter - getMsgByType: (type, arg1, arg2) -> + getMsgByType: (type, arg1 = {}, arg2) -> switch type when "CANNOT_TRASH_ASSETS" """ @@ -39,6 +39,8 @@ API = { """ when "BROWSER_NOT_FOUND" """ + Can't run because you've entered an invalid browser. + Browser: '#{arg1}' was not found on your system. Available browsers found are: #{arg2} @@ -135,13 +137,37 @@ API = { https://on.cypress.io/cypress-ci-deprecated """ + when "DASHBOARD_INVALID_RUN_REQUEST" + """ + Recording this run failed because the request was invalid. + + #{arg1.message} + + Errors: + + #{JSON.stringify(arg1.errors, null, 2)} + + Request Sent: + + #{JSON.stringify(arg1.object, null, 2)} + """ + when "RECORDING_FROM_FORK_PR" + """ + Warning: It looks like you are trying to record this run from a forked PR. + + The 'Record Key' is missing. Your CI provider is likely not passing private environment variables to builds from forks. + + These results will not be recorded. + + This error will not alter the exit code. + """ when "DASHBOARD_CANNOT_UPLOAD_RESULTS" """ Warning: We encountered an error while uploading results from your run. These results will not be recorded. - This error will not alter or the exit code. + This error will not alter the exit code. #{arg1} """ @@ -157,7 +183,7 @@ API = { """ when "RECORD_KEY_NOT_VALID" """ - We failed trying to authenticate this project. + We failed trying to authenticate this project: #{chalk.blue(arg2)} Your Record Key is invalid: #{chalk.yellow(arg1)} @@ -209,8 +235,25 @@ API = { #{chalk.yellow(arg2)} """ - when "SPEC_FILE_NOT_FOUND" - "Can't find test spec: " + chalk.blue(arg1) + when "NO_SPECS_FOUND" + ## no glob provided, searched all specs + if not arg2 + """ + Can't run because no spec files were found. + + We searched for any files inside of this folder: + + #{chalk.blue(arg1)} + """ + else + """ + Can't run because no spec files were found. + + We searched for any files matching this glob pattern: + + #{chalk.blue(arg2)} + """ + when "RENDERER_CRASHED" """ We detected that the Chromium Renderer process just crashed. @@ -326,11 +369,15 @@ API = { """ when "INVALID_REPORTER_NAME" """ - Could not load reporter by name: #{chalk.yellow(arg1)} + Could not load reporter by name: #{chalk.yellow(arg1.name)} We searched for the reporter in these paths: - #{listPaths(arg2).join("\n")} + #{listPaths(arg1.paths).join("\n")} + + The error we received was: + + #{chalk.yellow(arg1.error)} Learn more at https://on.cypress.io/reporters """ diff --git a/packages/server/lib/exception.coffee b/packages/server/lib/exception.coffee index 73d186ed838e..a8b3f09a2e55 100644 --- a/packages/server/lib/exception.coffee +++ b/packages/server/lib/exception.coffee @@ -1,7 +1,6 @@ _ = require("lodash") Promise = require("bluebird") winston = require("winston") -fs = require("fs-extra") pkg = require("@packages/root") api = require("./api") diff --git a/packages/server/lib/exec.coffee b/packages/server/lib/exec.coffee index d984426c7295..e0d13ef22d7f 100644 --- a/packages/server/lib/exec.coffee +++ b/packages/server/lib/exec.coffee @@ -48,6 +48,6 @@ module.exports = { .catch Promise.TimeoutError, -> msg = "Process timed out\ncommand: #{options.cmd}" err = new Error(msg) - err.timedout = true + err.timedOut = true throw err } diff --git a/packages/server/lib/files.coffee b/packages/server/lib/files.coffee index e54de6326963..c2f3d2e86b76 100644 --- a/packages/server/lib/files.coffee +++ b/packages/server/lib/files.coffee @@ -1,8 +1,6 @@ -fs = require("fs-extra") path = require("path") Promise = require("bluebird") - -fs = Promise.promisifyAll(fs) +fs = require("./util/fs") module.exports = { readFile: (projectRoot, file, options = {}) -> diff --git a/packages/server/lib/fixture.coffee b/packages/server/lib/fixture.coffee index 0c638932e3c2..442205ed4f09 100644 --- a/packages/server/lib/fixture.coffee +++ b/packages/server/lib/fixture.coffee @@ -1,13 +1,11 @@ _ = require("lodash") -fs = require("fs-extra") path = require("path") check = require("syntax-error") coffee = require("../../../packages/coffee") Promise = require("bluebird") jsonlint = require("jsonlint") cwd = require("./cwd") - -fs = Promise.promisifyAll(fs) +fs = require("./util/fs") extensions = ".json .js .coffee .html .txt .csv .png .jpg .jpeg .gif .tif .tiff .zip".split(" ") diff --git a/packages/server/lib/gui/events.coffee b/packages/server/lib/gui/events.coffee index 7528bea384b3..314dda806dfe 100644 --- a/packages/server/lib/gui/events.coffee +++ b/packages/server/lib/gui/events.coffee @@ -17,15 +17,15 @@ connect = require("../util/connect") konfig = require("../konfig") handleEvent = (options, bus, event, id, type, arg) -> - debug("got request for event:", type, arg) + debug("got request for event: %s, %o", type, arg) sendResponse = (data = {}) -> try - debug("sending ipc data", {type: type, data: data}) + debug("sending ipc data %o", {type: type, data: data}) event.sender.send("response", data) sendErr = (err) -> - debug("send error:", err) + debug("send error: %o", err) sendResponse({id: id, __error: errors.clone(err, {html: true})}) send = (data) -> @@ -101,9 +101,8 @@ handleEvent = (options, bus, event, id, type, arg) -> .catch(sendErr) when "launch:browser" - # headless.createWindows(arg, true) openProject.launch(arg.browser, arg.spec, { - projectPath: options.projectPath + projectRoot: options.projectRoot onBrowserOpen: -> send({browserOpened: true}) onBrowserClose: -> @@ -112,7 +111,7 @@ handleEvent = (options, bus, event, id, type, arg) -> .catch(sendErr) when "window:open" - Windows.open(options.projectPath, arg) + Windows.open(options.projectRoot, arg) .then(send) .catch(sendErr) diff --git a/packages/server/lib/gui/menu.coffee b/packages/server/lib/gui/menu.coffee index 4a247739752a..d64e80a810a6 100644 --- a/packages/server/lib/gui/menu.coffee +++ b/packages/server/lib/gui/menu.coffee @@ -14,7 +14,7 @@ module.exports = { withDevTools: false }) - ## this set by modes/headed.coffee and needs to be preserved if the menu + ## this set by modes/interactive.coffee and needs to be preserved if the menu ## is set again by launcher.coffee when the Electron browser is run if options.onLogOutClicked onLogOutClicked = options.onLogOutClicked diff --git a/packages/server/lib/gui/windows.coffee b/packages/server/lib/gui/windows.coffee index ea3ddd17c95c..a573fd9f1dfc 100644 --- a/packages/server/lib/gui/windows.coffee +++ b/packages/server/lib/gui/windows.coffee @@ -148,7 +148,7 @@ module.exports = { } }) - create: (projectPath, options = {}) -> + create: (projectRoot, options = {}) -> options = @defaults(options) if options.show is false @@ -177,7 +177,7 @@ module.exports = { options.onNewWindow.apply(win, arguments) if ts = options.trackState - @trackState(projectPath, win, ts) + @trackState(projectRoot, win, ts) ## open dev tools if they're true if options.devTools @@ -211,7 +211,7 @@ module.exports = { win - open: (projectPath, options = {}) -> + open: (projectRoot, options = {}) -> ## if we already have a window open based ## on that type then just show + focus it! if win = getByType(options.type) @@ -254,7 +254,7 @@ module.exports = { # args.width = 0 # args.height = 0 - win = @create(projectPath, options) + win = @create(projectRoot, options) debug("creating electron window with options %o", options) @@ -299,7 +299,7 @@ module.exports = { else return win - trackState: (projectPath, win, keys) -> + trackState: (projectRoot, win, keys) -> isDestroyed = -> win.isDestroyed() @@ -313,7 +313,7 @@ module.exports = { newState[keys.height] = height newState[keys.x] = x newState[keys.y] = y - savedState(projectPath) + savedState(projectRoot) .then (state) -> state.set(newState) , 500 @@ -325,7 +325,7 @@ module.exports = { newState = {} newState[keys.x] = x newState[keys.y] = y - savedState(projectPath) + savedState(projectRoot) .then (state) -> state.set(newState) , 500 @@ -333,14 +333,14 @@ module.exports = { win.webContents.on "devtools-opened", -> newState = {} newState[keys.devTools] = true - savedState(projectPath) + savedState(projectRoot) .then (state) -> state.set(newState) win.webContents.on "devtools-closed", -> newState = {} newState[keys.devTools] = false - savedState(projectPath) + savedState(projectRoot) .then (state) -> state.set(newState) diff --git a/packages/server/lib/ids.coffee b/packages/server/lib/ids.coffee deleted file mode 100644 index ba46f6cab80e..000000000000 --- a/packages/server/lib/ids.coffee +++ /dev/null @@ -1,48 +0,0 @@ -fs = require("fs") -path = require("path") -glob = require("glob") -Promise = require("bluebird") - -fs = Promise.promisifyAll(fs) -idRe = /\s*\[.{3}\]/g - -module.exports = { - files: (pathToTestFiles) -> - new Promise (resolve, reject) -> - glob path.join(pathToTestFiles, "**", "*+(.js|.coffee)"), {nodir: true}, (err, files) -> - reject(err) if err - - resolve(files) - - get: (pathToTestFiles) -> - getIds = (memo, file) -> - fs.readFileAsync(file, "utf8") - .then (contents) -> - if matches = contents.match(idRe) - memo = memo.concat(matches) - - memo - - Promise.reduce @files(pathToTestFiles), getIds, [] - - remove: (pathToTestFiles) -> - removeIds = (memo, file) -> - fs.readFileAsync(file, "utf8") - .then (contents) -> - if matches = contents.match(idRe) - ## add the number of matched ids - memo.ids += matches.length - - ## add to the total number of files - memo.files += 1 - - ## strip out all of the ids - contents = contents.replace(idRe, "") - - fs.writeFileAsync(file, contents) - - ## always return the memo object - memo - - Promise.reduce @files(pathToTestFiles), removeIds, {ids: 0, files: 0} -} \ No newline at end of file diff --git a/packages/server/lib/logger.coffee b/packages/server/lib/logger.coffee index b7f45d30b524..487c46af221e 100644 --- a/packages/server/lib/logger.coffee +++ b/packages/server/lib/logger.coffee @@ -1,8 +1,8 @@ path = require("path") _ = require("lodash") -fs = require("fs-extra") Promise = require("bluebird") winston = require("winston") +fs = require("./util/fs") appData = require("./util/app_data") folder = appData.path() @@ -165,4 +165,4 @@ process.on "unhandledRejection", (err, promise) -> return false -module.exports = logger \ No newline at end of file +module.exports = logger diff --git a/packages/server/lib/modes/headless.coffee b/packages/server/lib/modes/headless.coffee deleted file mode 100644 index d340d23c3453..000000000000 --- a/packages/server/lib/modes/headless.coffee +++ /dev/null @@ -1,567 +0,0 @@ -_ = require("lodash") -fs = require("fs-extra") -uuid = require("uuid") -path = require("path") -chalk = require("chalk") -human = require("human-interval") -Promise = require("bluebird") -random = require("randomstring") -pkg = require("@packages/root") -debug = require("debug")("cypress:server:headless") -ss = require("../screenshots") -user = require("../user") -stats = require("../stats") -video = require("../video") -errors = require("../errors") -Project = require("../project") -Reporter = require("../reporter") -openProject = require("../open_project") -progress = require("../util/progress_bar") -trash = require("../util/trash") -terminal = require("../util/terminal") -humanTime = require("../util/human_time") -Windows = require("../gui/windows") - -fs = Promise.promisifyAll(fs) - -TITLE_SEPARATOR = " /// " - -haveProjectIdAndKeyButNoRecordOption = (projectId, options) -> - ## if we have a project id - ## and we have a key - ## and (record or ci) hasn't been set to true or false - (projectId and options.key) and (_.isUndefined(options.record) and _.isUndefined(options.ci)) - -collectTestResults = (obj) -> - { - tests: obj.tests - passes: obj.passes - pending: obj.pending - failures: obj.failures - duration: humanTime(obj.duration) - screenshots: obj.screenshots and obj.screenshots.length - video: !!obj.video - version: pkg.version - } - -module.exports = { - collectTestResults - - getId: -> - ## return a random id - random.generate({ - length: 5 - capitalization: "lowercase" - }) - - getProjectId: (project, id) -> - ## if we have an ID just use it - if id - return Promise.resolve(id) - - project - .getProjectId() - .catch -> - ## no id no problem - return null - - openProject: (id, options) -> - ## now open the project to boot the server - ## putting our web client app in headless mode - ## - NO display server logs (via morgan) - ## - YES display reporter results (via mocha reporter) - openProject.create(options.projectPath, options, { - morgan: false - socketId: id - report: true - isTextTerminal: options.isTextTerminal ? true - onError: (err) -> - console.log() - console.log(err.stack) - openProject.emit("exitEarlyWithErr", err.message) - }) - .catch {portInUse: true}, (err) -> - ## TODO: this needs to move to emit exitEarly - ## so we record the failure in CI - errors.throw("PORT_IN_USE_LONG", err.port) - - createRecording: (name) -> - outputDir = path.dirname(name) - - fs.ensureDirAsync(outputDir) - .then -> - console.log("\nStarted video recording: #{chalk.cyan(name)}\n") - - video.start(name, { - onError: (err) -> - ## catch video recording failures and log them out - ## but don't let this affect the run at all - errors.warning("VIDEO_RECORDING_FAILED", err.stack) - }) - - getElectronProps: (showGui, project, write) -> - obj = { - width: 1280 - height: 720 - show: showGui - onCrashed: -> - err = errors.get("RENDERER_CRASHED") - errors.log(err) - - project.emit("exitEarlyWithErr", err.message) - onNewWindow: (e, url, frameName, disposition, options) -> - ## force new windows to automatically open with show: false - ## this prevents window.open inside of javascript client code - ## to cause a new BrowserWindow instance to open - ## https://github.com/cypress-io/cypress/issues/123 - options.show = false - } - - if write - obj.recordFrameRate = 20 - obj.onPaint = (event, dirty, image) -> - write(image.toJPEG(100)) - - obj - - displayStats: (obj = {}) -> - bgColor = if obj.failures then "bgRed" else "bgGreen" - color = if obj.failures then "red" else "green" - - console.log("") - - terminal.header("Tests Finished", { - color: [color] - }) - - console.log("") - - stats.display(color, obj) - - displayScreenshots: (screenshots = []) -> - console.log("") - console.log("") - - terminal.header("Screenshots", {color: ["yellow"]}) - - console.log("") - - format = (s) -> - dimensions = chalk.gray("(#{s.width}x#{s.height})") - - " - #{s.path} #{dimensions}" - - screenshots.forEach (screenshot) -> - console.log(format(screenshot)) - - postProcessRecording: (end, name, cname, videoCompression, shouldUploadVideo) -> - debug("ending the video recording:", name) - - ## once this ended promises resolves - ## then begin processing the file - end() - .then -> - ## dont process anything if videoCompress is off - ## or we've been told not to upload the video - return if videoCompression is false or shouldUploadVideo is false - - console.log("") - console.log("") - - terminal.header("Video", { - color: ["cyan"] - }) - - console.log("") - - # bar = progress.create("Post Processing Video") - console.log(" - Started processing: ", chalk.cyan("Compressing to #{videoCompression} CRF")) - - started = new Date - progress = Date.now() - tenSecs = human("10 seconds") - - onProgress = (float) -> - switch - when float is 1 - finished = new Date - started - duration = "(#{humanTime(finished)})" - console.log(" - Finished processing: ", chalk.cyan(name), chalk.gray(duration)) - - when (new Date - progress) > tenSecs - ## bump up the progress so we dont - ## continuously get notifications - progress += tenSecs - percentage = Math.ceil(float * 100) + "%" - console.log(" - Compression progress: ", chalk.cyan(percentage)) - - # bar.tickTotal(float) - - video.process(name, cname, videoCompression, onProgress) - .catch {recordingVideoFailed: true}, (err) -> - ## dont do anything if this error occured because - ## recording the video had already failed - return - .catch (err) -> - ## log that post processing was attempted - ## but failed and dont let this change the run exit code - errors.warning("VIDEO_POST_PROCESSING_FAILED", err.stack) - - launchBrowser: (options = {}) -> - { browser, spec, write, headed, project, screenshots } = options - - headed = !!headed - - browser ?= "electron" - - browserOpts = switch browser - when "electron" - @getElectronProps(headed, project, write) - else - {} - - browserOpts.automationMiddleware = { - onAfterResponse: (message, data, resp) => - if message is "take:screenshot" - screenshots.push @screenshotMetadata(data, resp) - - resp - } - - browserOpts.projectPath = options.projectPath - - openProject.launch(browser, spec, browserOpts) - - listenForProjectEnd: (project, headed, exit) -> - new Promise (resolve) -> - return if exit is false - - onEarlyExit = (errMsg) -> - ## probably should say we ended - ## early too: (Ended Early: true) - ## in the stats - obj = { - error: errors.stripAnsi(errMsg) - failures: 1 - tests: 0 - passes: 0 - pending: 0 - duration: 0 - failingTests: [] - } - - resolve(obj) - - onEnd = (obj) => - resolve(obj) - - ## when our project fires its end event - ## resolve the promise - project.once("end", onEnd) - project.once("exitEarlyWithErr", onEarlyExit) - - waitForBrowserToConnect: (options = {}) -> - { project, id, timeout } = options - - attempts = 0 - - do waitForBrowserToConnect = => - Promise.join( - @waitForSocketConnection(project, id) - @launchBrowser(options) - ) - .timeout(timeout ? 30000) - .catch Promise.TimeoutError, (err) => - attempts += 1 - - console.log("") - - ## always first close the open browsers - ## before retrying or dieing - openProject.closeBrowser() - .then -> - switch attempts - ## try again up to 3 attempts - when 1, 2 - word = if attempts is 1 then "Retrying..." else "Retrying again..." - errors.warning("TESTS_DID_NOT_START_RETRYING", word) - - waitForBrowserToConnect() - - else - err = errors.get("TESTS_DID_NOT_START_FAILED") - errors.log(err) - - project.emit("exitEarlyWithErr", err.message) - - waitForSocketConnection: (project, id) -> - new Promise (resolve, reject) -> - fn = (socketId) -> - if socketId is id - ## remove the event listener if we've connected - project.removeListener "socket:connected", fn - - ## resolve the promise - resolve() - - ## when a socket connects verify this - ## is the one that matches our id! - project.on "socket:connected", fn - - waitForTestsToFinishRunning: (options = {}) -> - { project, headed, screenshots, started, end, name, cname, videoCompression, videoUploadOnPasses, outputPath, exit } = options - - @listenForProjectEnd(project, headed, exit) - .then (obj) => - if end - obj.video = name - - if screenshots - obj.screenshots = screenshots - - testResults = collectTestResults(obj) - - writeOutput = -> - if not outputPath - return Promise.resolve() - - debug("saving results as %s", outputPath) - - fs.outputJsonAsync(outputPath, testResults) - - finish = -> - writeOutput() - .then -> - project.getConfig() - .then (cfg) -> - obj.config = cfg - .return(obj) - - @displayStats(testResults) - - if screenshots and screenshots.length - @displayScreenshots(screenshots) - - ft = obj.failingTests - - hasFailingTests = ft and ft.length - - if hasFailingTests - obj.failingTests = Reporter.setVideoTimestamp(started, ft) - - ## we should upload the video if we upload on passes (by default) - ## or if we have any failures - suv = obj.shouldUploadVideo = !!(videoUploadOnPasses is true or hasFailingTests) - - debug("attempting to close the browser") - - ## always close the browser now as opposed to letting - ## it exit naturally with the parent process due to - ## electron bug in windows - openProject.closeBrowser() - .then => - if end - @postProcessRecording(end, name, cname, videoCompression, suv) - .then(finish) - ## TODO: add a catch here - else - finish() - - trashAssets: (options = {}) -> - if options.trashAssetsBeforeHeadlessRuns is true - Promise.join( - trash.folder(options.videosFolder) - trash.folder(options.screenshotsFolder) - ) - .catch (err) -> - ## dont make trashing assets fail the build - errors.warning("CANNOT_TRASH_ASSETS", err.stack) - else - Promise.resolve() - - screenshotMetadata: (data, resp) -> - { - clientId: uuid.v4() - title: data.name ## TODO: rename this property - # name: data.name - testId: data.testId - testTitle: data.titles.join(TITLE_SEPARATOR) - path: resp.path - height: resp.height - width: resp.width - } - - copy: (videosFolder, screenshotsFolder) -> - Promise.try -> - ## dont attempt to copy if we're running in circle and we've turned off copying artifacts - shouldCopy = (ca = process.env.CIRCLE_ARTIFACTS) and process.env["COPY_CIRCLE_ARTIFACTS"] isnt "false" - - debug("Should copy Circle Artifacts?", Boolean(shouldCopy)) - - if shouldCopy and videosFolder and screenshotsFolder - debug("Copying Circle Artifacts", ca, videosFolder, screenshotsFolder) - - ## copy each of the screenshots and videos - ## to artifacts using each basename of the folders - Promise.join( - ss.copy( - screenshotsFolder, - path.join(ca, path.basename(screenshotsFolder)) - ), - video.copy( - videosFolder, - path.join(ca, path.basename(videosFolder)) - ) - ) - - allDone: -> - console.log("") - console.log("") - - terminal.header("All Done", { - color: ["gray"] - }) - - console.log("") - - runTests: (options = {}) -> - { browser, videoRecording, videosFolder } = options - debug("runTests with options", options) - - browser ?= "electron" - debug("runTests for browser #{browser}") - - screenshots = [] - - ## we know we're done running headlessly - ## when the renderer has connected and - ## finishes running all of the tests. - ## we're using an event emitter interface - ## to gracefully handle this in promise land - - ## if we've been told to record and we're not spawning a headed browser - browserCanBeRecorded = (name) -> - name is "electron" and not options.headed - - if videoRecording - if browserCanBeRecorded(browser) - if !videosFolder - throw new Error("Missing videoFolder for recording") - - id2 = @getId() - name = path.join(videosFolder, id2 + ".mp4") - cname = path.join(videosFolder, id2 + "-compressed.mp4") - recording = @createRecording(name) - else - if browser is "electron" and options.headed - errors.warning("CANNOT_RECORD_VIDEO_HEADED") - else - errors.warning("CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER", browser) - - Promise.resolve(recording) - .then (props = {}) => - ## extract the started + ended promises from recording - {start, end, write} = props - - terminal.header("Tests Starting", {color: ["gray"]}) - - ## make sure we start the recording first - ## before doing anything - Promise.resolve(start) - .then (started) => - Promise.props({ - stats: @waitForTestsToFinishRunning({ - exit: options.exit - headed: options.headed - project: options.project - videoCompression: options.videoCompression - videoUploadOnPasses: options.videoUploadOnPasses - outputPath: options.outputPath - end - name - cname - started - screenshots - }), - - connection: @waitForBrowserToConnect({ - id: options.id - spec: options.spec - headed: options.headed - project: options.project - webSecurity: options.webSecurity - projectPath: options.projectPath - write - browser - screenshots - }) - }) - - ready: (options = {}) -> - debug("headless mode ready with options %j", options) - - id = @getId() - - { projectPath } = options - - ## let's first make sure this project exists - Project.ensureExists(projectPath) - .then => - ## open this project without - ## adding it to the global cache - @openProject(id, options) - .call("getProject") - .then (project) => - Promise.all([ - @getProjectId(project, options.projectId) - - project.getConfig(), - ]) - .spread (projectId, config) => - ## if we have a project id and a key but record hasnt - ## been set - if haveProjectIdAndKeyButNoRecordOption(projectId, options) - ## log a warning telling the user - ## that they either need to provide us - ## with a RECORD_KEY or turn off - ## record mode - errors.warning("PROJECT_ID_AND_KEY_BUT_MISSING_RECORD_OPTION", projectId) - - @trashAssets(config) - .then => - @runTests({ - projectPath - id: id - project: project - videosFolder: config.videosFolder - videoRecording: config.videoRecording - videoCompression: config.videoCompression - videoUploadOnPasses: config.videoUploadOnPasses - exit: options.exit - spec: options.spec - headed: options.headed - browser: options.browser - outputPath: options.outputPath - }) - .get("stats") - .finally => - @copy(config.videosFolder, config.screenshotsFolder) - .then => - if options.allDone isnt false - @allDone() - - run: (options) -> - app = require("electron").app - - waitForReady = -> - new Promise (resolve, reject) -> - app.on "ready", resolve - - Promise.any([ - waitForReady() - Promise.delay(500) - ]) - .then => - @ready(options) - -} diff --git a/packages/server/lib/modes/index.coffee b/packages/server/lib/modes/index.coffee index d66eacf5a912..1d03a9ab7e74 100644 --- a/packages/server/lib/modes/index.coffee +++ b/packages/server/lib/modes/index.coffee @@ -2,7 +2,7 @@ module.exports = (mode, options) -> switch mode when "record" require("./record").run(options) - when "headless" - require("./headless").run(options) - when "headed" - require("./headed").run(options) + when "run" + require("./run").run(options) + when "interactive" + require("./interactive").run(options) diff --git a/packages/server/lib/modes/headed.coffee b/packages/server/lib/modes/interactive.coffee similarity index 95% rename from packages/server/lib/modes/headed.coffee rename to packages/server/lib/modes/interactive.coffee index dc72088fb441..bfe6484196b8 100644 --- a/packages/server/lib/modes/headed.coffee +++ b/packages/server/lib/modes/interactive.coffee @@ -73,7 +73,7 @@ module.exports = { ready: (options = {}) -> bus = new EE - { projectPath } = options + { projectRoot } = options ## TODO: potentially just pass an event emitter ## instance here instead of callback functions @@ -83,10 +83,10 @@ module.exports = { bus.emit("menu:item:clicked", "log:out") }) - savedState(projectPath) + savedState(projectRoot) .then (state) -> state.get() .then (state) => - Windows.open(projectPath, @getWindowArgs(state, options)) + Windows.open(projectRoot, @getWindowArgs(state, options)) .then (win) => Events.start(_.extend({}, options, { onFocusTests: -> win.focus() diff --git a/packages/server/lib/modes/record.coffee b/packages/server/lib/modes/record.coffee index 588d7123aca0..567b71df7f9c 100644 --- a/packages/server/lib/modes/record.coffee +++ b/packages/server/lib/modes/record.coffee @@ -1,20 +1,20 @@ _ = require("lodash") os = require("os") +la = require("lazy-ass") chalk = require("chalk") +check = require("check-more-types") +debug = require("debug")("cypress:server:record") Promise = require("bluebird") -headless = require("./headless") +isForkPr = require("is-fork-pr") +commitInfo = require("@cypress/commit-info") api = require("../api") logger = require("../logger") errors = require("../errors") -stdout = require("../stdout") +capture = require("../capture") upload = require("../upload") -Project = require("../project") +env = require("../util/env") terminal = require("../util/terminal") ciProvider = require("../util/ci_provider") -debug = require("debug")("cypress:server") -commitInfo = require("@cypress/commit-info") -la = require("lazy-ass") -check = require("check-more-types") logException = (err) -> ## give us up to 1 second to @@ -24,253 +24,342 @@ logException = (err) -> .catch -> ## dont yell about any errors either -module.exports = { - generateProjectBuildId: (projectId, projectPath, projectName, recordKey, group, groupId, specPattern) -> - if not recordKey - errors.throw("RECORD_KEY_MISSING") - if groupId and not group - console.log("Warning: you passed group-id but no group flag") - - la(check.maybe.unemptyString(specPattern), "invalid spec pattern", specPattern) - - debug("generating build id for project %s at %s", projectId, projectPath) - Promise.all([ - commitInfo.commitInfo(projectPath), - Project.findSpecs(projectPath, specPattern) - ]) - .spread (git, specs) -> - debug("git information") - debug(git) - if specPattern - debug("spec pattern", specPattern) - debug("project specs") - debug(specs) - la(check.maybe.strings(specs), "invalid list of specs to run", specs) - # only send groupId if group option is true - if group - groupId ?= ciProvider.groupId() +warnIfCiFlag = (ci) -> + ## if we are using the ci flag that means + ## we have an old version of the CLI tools installed + ## and that we need to warn the user what to update + if ci + type = switch + when env.get("CYPRESS_CI_KEY") + "CYPRESS_CI_DEPRECATED_ENV_VAR" else - groupId = null - createRunOptions = { - projectId: projectId - recordKey: recordKey - commitSha: git.sha - commitBranch: git.branch - commitAuthorName: git.author - commitAuthorEmail: git.email - commitMessage: git.message - remoteOrigin: git.remote - ciParams: ciProvider.params() - ciProvider: ciProvider.name() - ciBuildNumber: ciProvider.buildNum() - groupId: groupId - specs: specs - specPattern: specPattern - } - - api.createRun(createRunOptions) - .catch (err) -> - switch err.statusCode - when 401 - recordKey = recordKey.slice(0, 5) + "..." + recordKey.slice(-5) - errors.throw("RECORD_KEY_NOT_VALID", recordKey, projectId) - when 404 - errors.throw("DASHBOARD_PROJECT_NOT_FOUND", projectId) - else - ## warn the user that assets will be not recorded - errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - - ## report on this exception - ## and return null - logException(err) - .return(null) - - createInstance: (buildId, spec, browser) -> - api.createInstance({ - buildId - spec - browser - }) - .catch (err) -> - errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) + "CYPRESS_CI_DEPRECATED" - ## dont log exceptions if we have a 503 status code - if err.statusCode isnt 503 - logException(err) - .return(null) - else - null + errors.warning(type) - upload: (options = {}) -> - {video, uploadVideo, screenshots, videoUrl, screenshotUrls} = options +haveProjectIdAndKeyButNoRecordOption = (projectId, options) -> + ## if we have a project id + ## and we have a key + ## and (record or ci) hasn't been set to true or false + (projectId and options.key) and ( + _.isUndefined(options.record) and _.isUndefined(options.ci) + ) - uploads = [] - count = 0 +warnIfProjectIdButNoRecordOption = (projectId, options) -> + if haveProjectIdAndKeyButNoRecordOption(projectId, options) + ## log a warning telling the user + ## that they either need to provide us + ## with a RECORD_KEY or turn off + ## record mode + errors.warning("PROJECT_ID_AND_KEY_BUT_MISSING_RECORD_OPTION", projectId) - nums = -> - count += 1 +throwIfNoProjectId = (projectId) -> + if not projectId + errors.throw("CANNOT_RECORD_NO_PROJECT_ID") - chalk.gray("(#{count}/#{uploads.length})") +getSpecPath = (spec) -> + _.get(spec, "path") - send = (pathToFile, url) -> - success = -> - console.log(" - Done Uploading #{nums()}", chalk.blue(pathToFile)) +uploadArtifacts = (options = {}) -> + { video, screenshots, videoUploadUrl, shouldUploadVideo, screenshotUploadUrls } = options - fail = (err) -> - console.log(" - Failed Uploading #{nums()}", chalk.red(pathToFile)) + uploads = [] + count = 0 - uploads.push( - upload.send(pathToFile, url) - .then(success) - .catch(fail) - ) + nums = -> + count += 1 - if videoUrl and uploadVideo - send(video, videoUrl) + chalk.gray("(#{count}/#{uploads.length})") - if screenshotUrls - screenshotUrls.forEach (obj) -> - screenshot = _.find(screenshots, {clientId: obj.clientId}) + send = (pathToFile, url) -> + success = -> + console.log(" - Done Uploading #{nums()}", chalk.blue(pathToFile)) - send(screenshot.path, obj.uploadUrl) + fail = (err) -> + debug("failed to upload artifact %o", { + file: pathToFile + stack: err.stack + }) - if not uploads.length - console.log(" - Nothing to Upload") + console.log(" - Failed Uploading #{nums()}", chalk.red(pathToFile)) - Promise - .all(uploads) - .catch (err) -> - errors.warning("DASHBOARD_CANNOT_UPLOAD_RESULTS", err) + uploads.push( + upload.send(pathToFile, url) + .then(success) + .catch(fail) + ) - logException(err) + if videoUploadUrl and shouldUploadVideo + send(video, videoUploadUrl) + + if screenshotUploadUrls + screenshotUploadUrls.forEach (obj) -> + screenshot = _.find(screenshots, { screenshotId: obj.screenshotId }) + + send(screenshot.path, obj.uploadUrl) + + if not uploads.length + console.log(" - Nothing to Upload") + + Promise + .all(uploads) + .catch (err) -> + errors.warning("DASHBOARD_CANNOT_UPLOAD_RESULTS", err) - uploadAssets: (instanceId, stats, stdout) -> - console.log("") - console.log("") + logException(err) - terminal.header("Uploading Assets", { - color: ["blue"] +updateInstanceStdout = (options = {}) -> + { instanceId, captured } = options + + stdout = captured.toString() + + api.updateInstanceStdout({ + stdout + instanceId + }) + .catch (err) -> + debug("failed updating instance stdout %o", { + stack: err.stack }) - console.log("") - - ## get rid of the path property - screenshots = _.map stats.screenshots, (screenshot) -> - _.omit(screenshot, "path") - - api.updateInstance({ - instanceId: instanceId - tests: stats.tests - passes: stats.passes - failures: stats.failures - pending: stats.pending - duration: stats.duration - error: stats.error - video: !!stats.video - screenshots: screenshots - failingTests: stats.failingTests - cypressConfig: stats.config - ciProvider: ciProvider.name() ## TODO: don't send this (no reason to) - stdout: stdout + errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) + + ## dont log exceptions if we have a 503 status code + logException(err) unless err.statusCode is 503 + .finally(capture.restore) + +updateInstance = (options = {}) -> + { instanceId, results, captured } = options + { stats, tests, hooks, video, screenshots, reporterStats, error } = results + + video = Boolean(video) + cypressConfig = options.config + stdout = captured.toString() + + ## get rid of the path property + screenshots = _.map screenshots, (screenshot) -> + _.omit(screenshot, "path") + + api.updateInstance({ + stats + tests + error + video + hooks + stdout + instanceId + screenshots + reporterStats + cypressConfig + }) + .catch (err) -> + debug("failed updating instance %o", { + stack: err.stack }) - .then (resp = {}) => - @upload({ - video: stats.video - uploadVideo: stats.shouldUploadVideo - screenshots: stats.screenshots - videoUrl: resp.videoUploadUrl - screenshotUrls: resp.screenshotUploadUrls - }) - .catch (err) -> - errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - ## dont log exceptions if we have a 503 status code - if err.statusCode isnt 503 + errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) + + ## dont log exceptions if we have a 503 status code + if err.statusCode isnt 503 + logException(err) + .return(null) + else + null + +createRun = (options = {}) -> + { projectId, recordKey, platform, git, specPattern, specs } = options + + recordKey ?= env.get("CYPRESS_RECORD_KEY") or env.get("CYPRESS_CI_KEY") + + if not recordKey + if isForkPr.isForkPr() + ## bail with a warning + return errors.warning("RECORDING_FROM_FORK_PR") + + ## else throw + errors.throw("RECORD_KEY_MISSING") + + ## go back to being a string + if specPattern + specPattern = specPattern.join(",") + + specs = _.map(specs, getSpecPath) + + api.createRun({ + specPattern + specs + projectId + recordKey + platform + ci: { + params: ciProvider.params() + provider: ciProvider.name() + buildNumber: ciProvider.buildNum() + } + commit: { + sha: git.sha + branch: git.branch + authorName: git.author + authorEmail: git.email + message: git.message + remoteOrigin: git.remote + } + }) + .catch (err) -> + debug("failed creating run %o", { + stack: err.stack + }) + + switch err.statusCode + when 401 + recordKey = recordKey.slice(0, 5) + "..." + recordKey.slice(-5) + errors.throw("RECORD_KEY_NOT_VALID", recordKey, projectId) + when 404 + errors.throw("DASHBOARD_PROJECT_NOT_FOUND", projectId) + when 412 + errors.throw("DASHBOARD_INVALID_RUN_REQUEST", err.error) + else + ## warn the user that assets will be not recorded + errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) + + ## report on this exception + ## and return null logException(err) .return(null) - else - null - uploadStdout: (instanceId, stdout) -> - api.updateInstanceStdout({ - instanceId: instanceId - stdout: stdout +createInstance = (options = {}) -> + { runId, planId, machineId, platform, spec } = options + + spec = getSpecPath(spec) + + api.createInstance({ + spec + runId + planId + platform + machineId + }) + .catch (err) -> + debug("failed creating instance %o", { + stack: err.stack }) - .catch (err) -> - errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - - ## dont log exceptions if we have a 503 status code - logException(err) unless err.statusCode is 503 - - run: (options) -> - { projectPath, browser } = options - - ## default browser - browser ?= "electron" - - captured = stdout.capture() - - ## if we are using the ci flag that means - ## we have an old version of the CLI tools installed - ## and that we need to warn the user what to update - if options.ci - type = switch - when process.env.CYPRESS_CI_KEY - "CYPRESS_CI_DEPRECATED_ENV_VAR" - else - "CYPRESS_CI_DEPRECATED" - - errors.warning(type) - - Project.add(projectPath) - .then -> - Project.id(projectPath) - .catch -> - errors.throw("CANNOT_RECORD_NO_PROJECT_ID") - .then (projectId) => - ## store the projectId for later use - options.projectId = projectId - - Project.config(projectPath) - .then (cfg) => - { projectName } = cfg - - key = options.key ? process.env.CYPRESS_RECORD_KEY or process.env.CYPRESS_CI_KEY - - @generateProjectBuildId(projectId, projectPath, projectName, key, - options.group, options.groupId, options.spec) - .then (buildId) => - ## bail if we dont have a buildId - return if not buildId - - @createInstance(buildId, options.spec, browser) - .then (instanceId) => - ## dont check that the user is logged in - options.ensureAuthToken = false - - ## dont let headless say its all done - options.allDone = false - - didUploadAssets = false - - headless.run(options) - .then (stats = {}) => - ## if we got a instanceId then attempt to - ## upload these assets - if instanceId - @uploadAssets(instanceId, stats, captured.toString()) - .then (ret) -> - didUploadAssets = ret isnt null - .return(stats) - .finally => - headless.allDone() - - if didUploadAssets - stdout.restore() - @uploadStdout(instanceId, captured.toString()) - - else - stdout.restore() - headless.allDone() - return stats + + errors.warning("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) + + ## dont log exceptions if we have a 503 status code + if err.statusCode isnt 503 + logException(err) + .return(null) + else + null + +createRunAndRecordSpecs = (options = {}) -> + { specPattern, specs, sys, browser, projectId, projectRoot, runAllSpecs } = options + + recordKey = options.key + + commitInfo.commitInfo(projectRoot) + .then (git) -> + platform = { + osCpus: sys.osCpus + osName: sys.osName + osMemory: sys.osMemory + osVersion: sys.osVersion + browserName: browser.displayName + browserVersion: browser.version + } + + createRun({ + git + specs + platform + recordKey + projectId + specPattern + }) + .then (resp) -> + if not resp + runAllSpecs() + else + { runUrl, runId, machineId, planId } = resp + + captured = null + instanceId = null + + beforeSpecRun = (spec) -> + capture.restore() + + captured = capture.stdout() + + createInstance({ + spec + runId + planId + platform + machineId + }) + .then (id) -> + instanceId = id + + afterSpecRun = (results, config) -> + ## dont do anything if we failed to + ## create the instance + return if not instanceId + + console.log("") + + terminal.header("Uploading Results", { + color: ["blue"] + }) + + console.log("") + + updateInstance({ + config + results + captured + instanceId + }) + .then (resp) -> + return if not resp + + { video, shouldUploadVideo, screenshots } = results + { videoUploadUrl, screenshotUploadUrls } = resp + + uploadArtifacts({ + video + screenshots + videoUploadUrl + shouldUploadVideo + screenshotUploadUrls + }) + .finally -> + ## always attempt to upload stdout + ## even if uploading failed + updateInstanceStdout({ + captured + instanceId + }) + + runAllSpecs(beforeSpecRun, afterSpecRun, runUrl) + +module.exports = { + createRun + + createInstance + + updateInstance + + updateInstanceStdout + + uploadArtifacts + + warnIfCiFlag + + throwIfNoProjectId + + warnIfProjectIdButNoRecordOption + + createRunAndRecordSpecs + } diff --git a/packages/server/lib/modes/run.coffee b/packages/server/lib/modes/run.coffee new file mode 100644 index 000000000000..86f0c597a4e8 --- /dev/null +++ b/packages/server/lib/modes/run.coffee @@ -0,0 +1,918 @@ +_ = require("lodash") +pkg = require("@packages/root") +uuid = require("uuid") +path = require("path") +chalk = require("chalk") +human = require("human-interval") +debug = require("debug")("cypress:server:run") +Promise = require("bluebird") +logSymbols = require("log-symbols") +recordMode = require("./record") +video = require("../video") +errors = require("../errors") +Project = require("../project") +Reporter = require("../reporter") +browsers = require("../browsers") +openProject = require("../open_project") +Windows = require("../gui/windows") +fs = require("../util/fs") +env = require("../util/env") +trash = require("../util/trash") +random = require("../util/random") +system = require("../util/system") +progress = require("../util/progress_bar") +terminal = require("../util/terminal") +specsUtil = require("../util/specs") +humanTime = require("../util/human_time") +electronApp = require("../util/electron_app") + +color = (val, c) -> + chalk[c](val) + +gray = (val) -> + color(val, "gray") + +colorIf = (val, c) -> + if val is 0 + val = "-" + c = "gray" + + color(val, c) + +getSymbol = (num) -> + if num then logSymbols.error else logSymbols.success + +formatBrowser = (browser, headed) -> + isHeadless = browser.name is "electron" and not headed + + ## todo finish browser + _.compact([ + browser.displayName, + browser.majorVersion, + isHeadless and gray("(headless)") + ]).join(" ") + +formatFooterSummary = (results) -> + { totalFailed, runs } = results + + ## pass or fail color + c = if totalFailed then "red" else "green" + + phrase = do -> + ## if we have any specs failing... + if not totalFailed + return "All specs passed!" + + ## number of specs + total = runs.length + failingRuns = _.filter(runs, "stats.failures").length + percent = Math.round(failingRuns / total * 100) + + "#{failingRuns} of #{total} failed (#{percent}%)" + + return [ + color(phrase, c), + gray(humanTime.short(results.totalDuration)), + colorIf(results.totalTests, "reset"), + colorIf(results.totalPassed, "green"), + colorIf(totalFailed, "red"), + colorIf(results.totalPending, "cyan"), + colorIf(results.totalSkipped, "blue"), + ] + +formatSpecSummary = (name, failures) -> + [ + getSymbol(failures), + color(name, "reset") + ] + .join(" ") + +formatSpecPattern = (specPattern) -> + if specPattern + specPattern.join(", ") + +formatSpecs = (specs) -> + names = _.map(specs, "name") + + ## 25 found: (foo.spec.js, bar.spec.js, baz.spec.js) + [ + "#{names.length} found " + gray("("), + gray(names.join(', ')), + gray(")") + ] + .join("") + +displayRunStarting = (options = {}) -> + { specs, specPattern, browser, headed, runUrl } = options + + console.log("") + + terminal.divider("=") + + console.log("") + + terminal.header("Run Starting", { + color: ["reset"] + }) + + console.log("") + + table = terminal.table({ + colWidths: [12, 88] + type: "outsideBorder" + }) + + data = _ + .chain([ + [gray("Cypress:"), pkg.version] + [gray("Browser:"), formatBrowser(browser, headed)] + [gray("Specs:"), formatSpecs(specs)] + [gray("Searched:"), formatSpecPattern(specPattern)] + [gray("Run URL:"), runUrl] + ]) + .filter(_.property(1)) + .value() + + table.push(data...) + + console.log(table.toString()) + + console.log("") + +displaySpecHeader = (name, curr, total) -> + console.log("") + + table = terminal.table({ + colWidths: [80, 20] + colAligns: ["left", "right"] + type: "pageDivider" + style: { + "padding-left": 2 + } + }) + + table.push(["", ""]) + table.push([ + "Running: " + gray(name + "..."), + gray("(#{curr} of #{total})") + ]) + + console.log(table.toString()) + +collectTestResults = (obj = {}) -> + { + name: _.get(obj, 'spec.name') + tests: _.get(obj, 'stats.tests') + passes: _.get(obj, 'stats.passes') + pending: _.get(obj, 'stats.pending') + failures: _.get(obj, 'stats.failures') + skipped: _.get(obj, 'stats.skipped' ) + duration: humanTime.long(_.get(obj, 'stats.wallClockDuration')) + screenshots: obj.screenshots and obj.screenshots.length + video: Boolean(obj.video) + } + +renderSummaryTable = (runUrl, results) -> + { runs } = results + + console.log("") + + terminal.divider("=") + + console.log("") + + terminal.header("Run Finished", { + color: ["reset"] + }) + + if runs and runs.length + head = [" Spec", "", "Tests", "Passing", "Failing", "Pending", "Skipped"] + colAligns = ["left", "right", "right", "right", "right", "right", "right"] + colWidths = [40, 10, 10, 10, 10, 10, 10] + + table1 = terminal.table({ + colAligns + colWidths + type: "noBorder" + head: _.map(head, gray) + }) + + table2 = terminal.table({ + colAligns + colWidths + type: "border" + }) + + table3 = terminal.table({ + colAligns + colWidths + type: "noBorder" + head: formatFooterSummary(results) + style: { + "padding-right": 2 + } + }) + + _.each runs, (run) -> + { spec, stats } = run + + ms = humanTime.short(stats.wallClockDuration) + + table2.push([ + formatSpecSummary(spec.name, stats.failures) + color(ms, "gray") + colorIf(stats.tests, "reset") + colorIf(stats.passes, "green"), + colorIf(stats.failures, "red"), + colorIf(stats.pending, "cyan"), + colorIf(stats.skipped, "blue") + ]) + + console.log("") + console.log("") + console.log(terminal.renderTables(table1, table2, table3)) + console.log("") + + if runUrl + console.log("") + + table4 = terminal.table({ + colWidths: [100] + type: "pageDivider" + style: { + "padding-left": 2 + } + }) + + table4.push(["", ""]) + table4.push(["Recorded Run: " + gray(runUrl)]) + + console.log(terminal.renderTables(table4)) + console.log("") + +getProjectId = (project, id) -> + id ?= env.get("CYPRESS_PROJECT_ID") + + ## if we have an ID just use it + if id + return Promise.resolve(id) + + project + .getProjectId() + .catch -> + ## no id no problem + return null + +reduceRuns = (runs, prop) -> + _.reduce runs, (memo, run) -> + memo += _.get(run, prop) + , 0 + +getRun = (run, prop) -> + _.get(run, prop) + +writeOutput = (outputPath, results) -> + Promise.try -> + return if not outputPath + + debug("saving output results as %s", outputPath) + + fs.outputJsonAsync(outputPath, results) + +openProjectCreate = (projectRoot, socketId, options) -> + ## now open the project to boot the server + ## putting our web client app in headless mode + ## - NO display server logs (via morgan) + ## - YES display reporter results (via mocha reporter) + openProject.create(projectRoot, options, { + socketId + morgan: false + report: true + isTextTerminal: options.isTextTerminal ? true + onError: (err) -> + console.log("") + console.log(err.stack) + openProject.emit("exitEarlyWithErr", err.message) + }) + .catch {portInUse: true}, (err) -> + ## TODO: this needs to move to emit exitEarly + ## so we record the failure in CI + errors.throw("PORT_IN_USE_LONG", err.port) + +createAndOpenProject = (socketId, options) -> + { projectRoot, projectId } = options + + Project + .ensureExists(projectRoot) + .then -> + ## open this project without + ## adding it to the global cache + openProjectCreate(projectRoot, socketId, options) + .call("getProject") + .then (project) -> + Promise.props({ + project + config: project.getConfig() + projectId: getProjectId(project, projectId) + }) + +trashAssets = (config = {}) -> + if config.trashAssetsBeforeHeadlessRuns isnt true + return Promise.resolve() + + Promise.join( + trash.folder(config.videosFolder) + trash.folder(config.screenshotsFolder) + ) + .catch (err) -> + ## dont make trashing assets fail the build + errors.warning("CANNOT_TRASH_ASSETS", err.stack) + +module.exports = { + collectTestResults + + getProjectId + + writeOutput + + openProjectCreate + + createRecording: (name) -> + outputDir = path.dirname(name) + + fs + .ensureDirAsync(outputDir) + .then -> + video.start(name, { + onError: (err) -> + ## catch video recording failures and log them out + ## but don't let this affect the run at all + errors.warning("VIDEO_RECORDING_FAILED", err.stack) + }) + + getElectronProps: (showGui, project, write) -> + obj = { + width: 1280 + height: 720 + show: showGui + onCrashed: -> + err = errors.get("RENDERER_CRASHED") + errors.log(err) + + project.emit("exitEarlyWithErr", err.message) + onNewWindow: (e, url, frameName, disposition, options) -> + ## force new windows to automatically open with show: false + ## this prevents window.open inside of javascript client code + ## to cause a new BrowserWindow instance to open + ## https://github.com/cypress-io/cypress/issues/123 + options.show = false + } + + if write + obj.recordFrameRate = 20 + obj.onPaint = (event, dirty, image) -> + write(image.toJPEG(100)) + + obj + + displayResults: (obj = {}) -> + results = collectTestResults(obj) + + c = if results.failures then "red" else "green" + + console.log("") + + terminal.header("Results", { + color: [c] + }) + + table = terminal.table({ + type: "outsideBorder" + }) + + data = _.map [ + ["Tests:", results.tests] + ["Passing:", results.passes] + ["Failing:", results.failures] + ["Pending:", results.pending] + ["Skipped:", results.skipped] + ["Screenshots:", results.screenshots] + ["Video:", results.video] + ["Duration:", results.duration] + ["Spec Ran:", results.name] + ], (arr) -> + [key, val] = arr + + [color(key, "gray"), color(val, c)] + + table.push(data...) + + console.log("") + console.log(table.toString()) + console.log("") + + displayScreenshots: (screenshots = []) -> + console.log("") + + terminal.header("Screenshots", {color: ["yellow"]}) + + console.log("") + + format = (s) -> + dimensions = gray("(#{s.width}x#{s.height})") + + " - #{s.path} #{dimensions}" + + screenshots.forEach (screenshot) -> + console.log(format(screenshot)) + + console.log("") + + postProcessRecording: (end, name, cname, videoCompression, shouldUploadVideo) -> + debug("ending the video recording %o", { name, videoCompression, shouldUploadVideo }) + + ## once this ended promises resolves + ## then begin processing the file + end() + .then -> + ## dont process anything if videoCompress is off + ## or we've been told not to upload the video + return if videoCompression is false or shouldUploadVideo is false + + console.log("") + + terminal.header("Video", { + color: ["cyan"] + }) + + console.log("") + + # bar = progress.create("Post Processing Video") + console.log( + gray(" - Started processing: "), + chalk.cyan("Compressing to #{videoCompression} CRF") + ) + + started = new Date + progress = Date.now() + tenSecs = human("10 seconds") + + onProgress = (float) -> + switch + when float is 1 + finished = new Date - started + duration = "(#{humanTime.long(finished)})" + console.log( + gray(" - Finished processing: "), + chalk.cyan(name), + gray(duration) + ) + console.log("") + + when (new Date - progress) > tenSecs + ## bump up the progress so we dont + ## continuously get notifications + progress += tenSecs + percentage = Math.ceil(float * 100) + "%" + console.log(" - Compression progress: ", chalk.cyan(percentage)) + + # bar.tickTotal(float) + + video.process(name, cname, videoCompression, onProgress) + .catch {recordingVideoFailed: true}, (err) -> + ## dont do anything if this error occured because + ## recording the video had already failed + return + .catch (err) -> + ## log that post processing was attempted + ## but failed and dont let this change the run exit code + errors.warning("VIDEO_POST_PROCESSING_FAILED", err.stack) + + launchBrowser: (options = {}) -> + { browserName, spec, write, headed, project, screenshots } = options + + headed = !!headed + + browserOpts = switch browserName + when "electron" + @getElectronProps(headed, project, write) + else + {} + + browserOpts.automationMiddleware = { + onAfterResponse: (message, data, resp) => + if message is "take:screenshot" and resp + screenshots.push @screenshotMetadata(data, resp) + + resp + } + + browserOpts.projectRoot = options.projectRoot + + openProject.launch(browserName, spec.absolute, browserOpts) + + listenForProjectEnd: (project, headed, exit) -> + new Promise (resolve) -> + if exit is false + resolve = (arg) -> + console.log("not exiting due to options.exit being false") + + onEarlyExit = (errMsg) -> + ## probably should say we ended + ## early too: (Ended Early: true) + ## in the stats + obj = { + error: errors.stripAnsi(errMsg) + stats: { + failures: 1 + tests: 0 + passes: 0 + pending: 0 + suites: 0 + skipped: 0 + wallClockDuration: 0 + wallClockStartedAt: (new Date()).toJSON() + wallClockEndedAt: (new Date()).toJSON() + } + } + + resolve(obj) + + onEnd = (obj) -> + resolve(obj) + + ## when our project fires its end event + ## resolve the promise + project.once("end", onEnd) + project.once("exitEarlyWithErr", onEarlyExit) + + waitForBrowserToConnect: (options = {}) -> + { project, socketId, timeout } = options + + attempts = 0 + + do waitForBrowserToConnect = => + Promise.join( + @waitForSocketConnection(project, socketId) + @launchBrowser(options) + ) + .timeout(timeout ? 30000) + .catch Promise.TimeoutError, (err) => + attempts += 1 + + console.log("") + + ## always first close the open browsers + ## before retrying or dieing + openProject.closeBrowser() + .then -> + switch attempts + ## try again up to 3 attempts + when 1, 2 + word = if attempts is 1 then "Retrying..." else "Retrying again..." + errors.warning("TESTS_DID_NOT_START_RETRYING", word) + + waitForBrowserToConnect() + + else + err = errors.get("TESTS_DID_NOT_START_FAILED") + errors.log(err) + + project.emit("exitEarlyWithErr", err.message) + + waitForSocketConnection: (project, id) -> + new Promise (resolve, reject) -> + fn = (socketId) -> + if socketId is id + ## remove the event listener if we've connected + project.removeListener("socket:connected", fn) + + ## resolve the promise + resolve() + + ## when a socket connects verify this + ## is the one that matches our id! + project.on("socket:connected", fn) + + waitForTestsToFinishRunning: (options = {}) -> + { project, headed, screenshots, started, end, name, cname, videoCompression, videoUploadOnPasses, exit, spec } = options + + @listenForProjectEnd(project, headed, exit) + .then (obj) => + _.defaults(obj, { + error: null + hooks: null + tests: null + video: null + screenshots: null + reporterStats: null + }) + + if end + obj.video = name + + if screenshots + obj.screenshots = screenshots + + obj.spec = spec + + finish = -> + return obj + + @displayResults(obj) + + if screenshots and screenshots.length + @displayScreenshots(screenshots) + + { tests, stats } = obj + + failingTests = _.filter(tests, { state: "failed" }) + + hasFailingTests = _.get(stats, 'failures') > 0 + + ## if we have a video recording + if started and tests and tests.length + ## always set the video timestamp on tests + obj.tests = Reporter.setVideoTimestamp(started, tests) + + ## we should upload the video if we upload on passes (by default) + ## or if we have any failures and have started the video + suv = Boolean(videoUploadOnPasses is true or (started and hasFailingTests)) + + obj.shouldUploadVideo = suv + + debug("attempting to close the browser") + + ## always close the browser now as opposed to letting + ## it exit naturally with the parent process due to + ## electron bug in windows + openProject.closeBrowser() + .then => + if end + @postProcessRecording(end, name, cname, videoCompression, suv) + .then(finish) + ## TODO: add a catch here + else + finish() + + screenshotMetadata: (data, resp) -> + { + screenshotId: random.id() + name: data.name ? null + testId: data.testId + takenAt: resp.takenAt + path: resp.path + height: resp.dimensions.height + width: resp.dimensions.width + } + + runSpecs: (options = {}) -> + { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl } = options + + results = { + startedTestsAt: null + endedTestsAt: null + totalDuration: null + totalSuites: null, + totalTests: null, + totalFailed: null, + totalPassed: null, + totalPending: null, + totalSkipped: null, + runs: null, + browserPath: browser.path, + browserName: browser.name, + browserVersion: browser.version, + osName: sys.osName, + osVersion: sys.osVersion, + cypressVersion: pkg.version, + config, + } + + displayRunStarting({ + specs + runUrl + headed + browser + specPattern + }) + + runEachSpec = (spec, index, length) => + Promise + .try -> + if beforeSpecRun + debug("before spec run %o", spec) + + beforeSpecRun(spec) + .then => + displaySpecHeader(spec.name, index + 1, length) + + @runSpec(spec, options) + .get("results") + .tap (results) -> + debug("spec results %o", results) + + if afterSpecRun + debug("after spec run %o", spec) + + afterSpecRun(results, config) + + Promise + .mapSeries(specs, runEachSpec) + .then (runs = []) -> + results.startedTestsAt = start = getRun(_.first(runs), "stats.wallClockStartedAt") + results.endedTestsAt = end = getRun(_.last(runs), "stats.wallClockEndedAt") + results.totalDuration = reduceRuns(runs, "stats.wallClockDuration") + results.totalSuites = reduceRuns(runs, "stats.suites") + results.totalTests = reduceRuns(runs, "stats.tests") + results.totalPassed = reduceRuns(runs, "stats.passes") + results.totalPending = reduceRuns(runs, "stats.pending") + results.totalFailed = reduceRuns(runs, "stats.failures") + results.totalSkipped = reduceRuns(runs, "stats.skipped") + results.runs = runs + + debug("final results of all runs: %o", results) + + writeOutput(outputPath, results) + .return(results) + + runSpec: (spec = {}, options = {}) -> + { project, headed, browser, videoRecording, videosFolder } = options + + browserName = browser.name + + debug("about to run spec %o", { + spec + headed + browserName + }) + + screenshots = [] + + ## we know we're done running headlessly + ## when the renderer has connected and + ## finishes running all of the tests. + ## we're using an event emitter interface + ## to gracefully handle this in promise land + + ## if we've been told to record and we're not spawning a headed browser + browserCanBeRecorded = (name) -> + name is "electron" and not options.headed + + if videoRecording + if browserCanBeRecorded(browserName) + if not videosFolder + throw new Error("Missing videoFolder for recording") + + name = path.join(videosFolder, spec.name + ".mp4") + cname = path.join(videosFolder, spec.name + "-compressed.mp4") + + recording = @createRecording(name) + else + console.log("") + + if browserName is "electron" and options.headed + errors.warning("CANNOT_RECORD_VIDEO_HEADED") + else + errors.warning("CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER", browserName) + + Promise.resolve(recording) + .then (props = {}) => + ## extract the started + ended promises from recording + {start, end, write} = props + + ## make sure we start the recording first + ## before doing anything + Promise.resolve(start) + .then (started) => + Promise.props({ + results: @waitForTestsToFinishRunning({ + end + name + spec + cname + headed + started + project + screenshots + exit: options.exit + videoCompression: options.videoCompression + videoUploadOnPasses: options.videoUploadOnPasses + }), + + connection: @waitForBrowserToConnect({ + spec + write + headed + project + screenshots + browserName + socketId: options.socketId + webSecurity: options.webSecurity + projectRoot: options.projectRoot + }) + }) + + findSpecs: (config, specPattern) -> + specsUtil.find(config, specPattern) + .tap (specs = []) => + if debug.enabled + names = _.map(specs, "name") + debug( + "found '%d' specs using spec pattern '%s': %o", + names.length, + specPattern, + names + ) + + ready: (options = {}) -> + debug("run mode ready with options %o", options) + + _.defaults(options, { + browser: "electron" + }) + + socketId = random.id() + + { projectRoot, record, key } = options + + browserName = options.browser + + ## alias and coerce to null + specPattern = options.spec ? null + + ## warn if we're using deprecated --ci flag + recordMode.warnIfCiFlag(options.ci) + + ## ensure the project exists + ## and open up the project + createAndOpenProject(socketId, options) + .then ({ project, projectId, config }) => + ## if we have a project id and a key but record hasnt been given + recordMode.warnIfProjectIdButNoRecordOption(projectId, options) + + if record + recordMode.throwIfNoProjectId(projectId) + + Promise.all([ + system.info(), + browsers.ensureAndGetByName(browserName), + @findSpecs(config, specPattern), + trashAssets(config), + ]) + .spread (sys = {}, browser = {}, specs = []) => + ## return only what is return to the specPattern + if specPattern + specPattern = specsUtil.getPatternRelativeToProjectRoot(specPattern, projectRoot) + + if not specs.length + errors.throw('NO_SPECS_FOUND', config.integrationFolder, specPattern) + + runAllSpecs = (beforeSpecRun, afterSpecRun, runUrl) => + @runSpecs({ + beforeSpecRun + afterSpecRun + projectRoot + specPattern + socketId + browser + project + runUrl + config + specs + sys + videosFolder: config.videosFolder + videoRecording: config.videoRecording + videoCompression: config.videoCompression + videoUploadOnPasses: config.videoUploadOnPasses + exit: options.exit + headed: options.headed + outputPath: options.outputPath + }) + .tap(_.partial(renderSummaryTable, runUrl)) + + if record + { projectName } = config + + recordMode.createRunAndRecordSpecs({ + key + sys + specs + browser + projectId + projectRoot + projectName + specPattern + runAllSpecs + }) + else + runAllSpecs() + + run: (options) -> + electronApp + .ready() + .then => + @ready(options) + +} diff --git a/packages/server/lib/open_project.coffee b/packages/server/lib/open_project.coffee index cf7d299b199e..3559aaf85bbd 100644 --- a/packages/server/lib/open_project.coffee +++ b/packages/server/lib/open_project.coffee @@ -4,7 +4,9 @@ files = require("./controllers/files") config = require("./config") Project = require("./project") browsers = require("./browsers") -log = require('./log') +specsUtil = require('./util/specs') +log = require('debug')("cypress:server:project") +preprocessor = require("./plugins/preprocessor") create = -> openProject = null @@ -45,7 +47,7 @@ create = -> ## of potential domain changes, request buffers, etc @reset() .then -> - openProject.ensureSpecUrl(spec) + openProject.getSpecUrl(spec) .then (url) -> openProject.getConfig() .then (cfg) -> @@ -65,6 +67,13 @@ create = -> if am = options.automationMiddleware automation.use(am) + onBrowserClose = options.onBrowserClose + options.onBrowserClose = -> + if spec + preprocessor.removeFile(spec, cfg) + if onBrowserClose + onBrowserClose() + do relaunchBrowser = -> log "launching project in browser #{browserName}" browsers.open(browserName, options, automation) @@ -95,7 +104,13 @@ create = -> get = -> openProject.getConfig() .then (cfg) -> - files.getTestFiles(cfg) + specsUtil.find(cfg) + .then (specs = []) -> + ## TODO: put back 'integration' property + ## on the specs + return { + integration: specs + } specIntervalId = setInterval(checkForSpecUpdates, 2500) diff --git a/packages/server/lib/plugins/child/preprocessor.js b/packages/server/lib/plugins/child/preprocessor.js index ed41043fe6e7..9023b75ac2ce 100644 --- a/packages/server/lib/plugins/child/preprocessor.js +++ b/packages/server/lib/plugins/child/preprocessor.js @@ -2,27 +2,35 @@ const _ = require('lodash') const EE = require('events') const util = require('../util') -const configs = {} +const fileObjects = {} const wrap = (ipc, invoke, ids, args) => { - const config = _.pick(args[0], 'filePath', 'outputPath', 'shouldWatch') - let enhancedConfig = configs[config.filePath] - if (!enhancedConfig) { - enhancedConfig = configs[config.filePath] = _.extend(new EE(), config) - enhancedConfig.on('rerun', () => { - ipc.send('preprocessor:rerun', config.filePath) + const file = _.pick(args[0], 'filePath', 'outputPath', 'shouldWatch') + let childFile = fileObjects[file.filePath] + // the emitter methods don't come through from the parent process + // so we have to re-apply them here + if (!childFile) { + childFile = fileObjects[file.filePath] = _.extend(new EE(), file) + childFile.on('rerun', () => { + ipc.send('preprocessor:rerun', file.filePath) }) ipc.on('preprocessor:close', (filePath) => { // no filePath means close all - if (!filePath || filePath === config.filePath) { - delete configs[filePath] - enhancedConfig.emit('close') + if (!filePath || filePath === file.filePath) { + delete fileObjects[file.filePath] + childFile.emit('close') } }) } - util.wrapChildPromise(ipc, invoke, ids, [enhancedConfig]) + util.wrapChildPromise(ipc, invoke, ids, [childFile]) } module.exports = { wrap, + + // for testing purposes + _clearFiles: () => { + for (let file in fileObjects) delete fileObjects[file] + }, + _getFiles: () => fileObjects, } diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index 756bb501cd1f..8395845b59d5 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -1,18 +1,19 @@ -const log = require('debug')('cypress:server:plugins:child') +const debug = require('debug')('cypress:server:plugins:child') const Promise = require('bluebird') const preprocessor = require('./preprocessor') +const task = require('./task') const util = require('../util') -const callbacks = {} +const registeredEvents = {} -const invoke = (callbackId, args = []) => { - const callback = callbacks[callbackId] - if (!callback) { - sendError(new Error(`No callback registered for callback id ${callbackId}`)) +const invoke = (eventId, args = []) => { + const event = registeredEvents[eventId] + if (!event) { + sendError(new Error(`No handler registered for event id ${eventId}`)) return } - return callback(...args) + return event.handler(...args) } const sendError = (ipc, err) => { @@ -22,25 +23,29 @@ const sendError = (ipc, err) => { let plugins const load = (ipc, config, pluginsFile) => { - log('run plugins function') + debug('run plugins function') - let callbackIdCount = 0 + let eventIdCount = 0 const registrations = [] // we track the register calls and then send them all at once // to the parent process - const register = (event, fn) => { - const callbackId = callbackIdCount++ - callbacks[callbackId] = fn + const register = (event, handler) => { + const eventId = eventIdCount++ + registeredEvents[eventId] = { event, handler } - log('register event', event, 'with id', callbackId) + debug('register event', event, 'with id', eventId) registrations.push({ event, - callbackId, + eventId, }) } + // events used for parent/child communication + register('_get:task:body', () => {}) + register('_get:task:keys', () => {}) + Promise .try(() => { return plugins(register, config) @@ -54,7 +59,7 @@ const load = (ipc, config, pluginsFile) => { } const execute = (ipc, event, ids, args = []) => { - log('execute plugin with id', ids.invocationId) + debug(`execute plugin event: ${event} (%o)`, ids) switch (event) { case 'file:preprocessor': @@ -63,39 +68,48 @@ const execute = (ipc, event, ids, args = []) => { case 'before:browser:launch': util.wrapChildPromise(ipc, invoke, ids, args) return + case 'task': + task.wrap(ipc, registeredEvents, ids, args) + return + case '_get:task:keys': + task.getKeys(ipc, registeredEvents, ids) + return + case '_get:task:body': + task.getBody(ipc, registeredEvents, ids, args) + return default: - log('unexpected execute message:', event, args) + debug('unexpected execute message:', event, args) return } } module.exports = (ipc, pluginsFile) => { - log('pluginsFile:', pluginsFile) + debug('pluginsFile:', pluginsFile) process.on('uncaughtException', (err) => { - log('uncaught exception:', util.serializeError(err)) + debug('uncaught exception:', util.serializeError(err)) ipc.send('error', util.serializeError(err)) return false }) process.on('unhandledRejection', (event) => { const err = (event && event.reason) || event - log('unhandled rejection:', util.serializeError(err)) + debug('unhandled rejection:', util.serializeError(err)) ipc.send('error', util.serializeError(err)) return false }) try { - log('require pluginsFile') + debug('require pluginsFile') plugins = require(pluginsFile) } catch (err) { - log('failed to require pluginsFile:\n%s', err.stack) + debug('failed to require pluginsFile:\n%s', err.stack) ipc.send('load:error', 'PLUGINS_FILE_ERROR', pluginsFile, err.stack) return } if (typeof plugins !== 'function') { - log('not a function') + debug('not a function') ipc.send('load:error', 'PLUGINS_DIDNT_EXPORT_FUNCTION', pluginsFile, plugins) return } diff --git a/packages/server/lib/plugins/child/task.js b/packages/server/lib/plugins/child/task.js new file mode 100644 index 000000000000..27a794bffe31 --- /dev/null +++ b/packages/server/lib/plugins/child/task.js @@ -0,0 +1,38 @@ +const _ = require('lodash') +const util = require('../util') + +const getBody = (ipc, events, ids, [event]) => { + const taskEvent = _.find(events, { event: 'task' }).handler + const invoke = () => taskEvent[event].toString() + + util.wrapChildPromise(ipc, invoke, ids) +} + +const getKeys = (ipc, events, ids) => { + const taskEvent = _.find(events, { event: 'task' }).handler + const invoke = () => _.keys(taskEvent) + + util.wrapChildPromise(ipc, invoke, ids) +} + +const wrap = (ipc, events, ids, args) => { + const task = args[0] + const arg = args[1] + + const invoke = (eventId, args = []) => { + const handler = _.get(events, `${eventId}.handler.${task}`) + if (_.isFunction(handler)) { + return handler(...args) + } else { + return '__cypress_unhandled__' + } + } + + util.wrapChildPromise(ipc, invoke, ids, [arg]) +} + +module.exports = { + getBody, + getKeys, + wrap, +} diff --git a/packages/server/lib/plugins/index.coffee b/packages/server/lib/plugins/index.coffee index c8bba92db8b3..7be7ddd4e8d8 100644 --- a/packages/server/lib/plugins/index.coffee +++ b/packages/server/lib/plugins/index.coffee @@ -1,7 +1,7 @@ _ = require("lodash") cp = require("child_process") path = require("path") -log = require("debug")("cypress:server:plugins") +debug = require("debug")("cypress:server:plugins") Promise = require("bluebird") errors = require("../errors") util = require("./util") @@ -11,7 +11,7 @@ registeredEvents = {} handlers = [] register = (event, callback) -> - log("register event '#{event}'") + debug("register event '#{event}'") if not _.isString(event) throw new Error("The plugin register function must be called with an event as its 1st argument. You passed '#{event}'.") @@ -26,18 +26,18 @@ module.exports = { handlers.push(handler) init: (config, options) -> - log("plugins.init", config.pluginsFile) + debug("plugins.init", config.pluginsFile) new Promise (resolve, reject) -> return resolve() if not config.pluginsFile if pluginsProcess - log("kill existing plugins process") + debug("kill existing plugins process") pluginsProcess.kill() registeredEvents = {} - pluginsProcess = cp.fork(path.join(__dirname, "child", "index.js"), ["--file", config.pluginsFile]) + pluginsProcess = cp.fork(path.join(__dirname, "child", "index.js"), ["--file", config.pluginsFile], { stdio: "inherit" }) ipc = util.wrapIpc(pluginsProcess) handler(ipc) for handler in handlers @@ -46,12 +46,13 @@ module.exports = { ipc.on "loaded", (newCfg, registrations) -> _.each registrations, (registration) -> - log("register plugins process event", registration.event, "with id", registration.callbackId) + debug("register plugins process event", registration.event, "with id", registration.eventId) + register registration.event, (args...) -> - util.wrapParentPromise ipc, registration.callbackId, (invocationId) -> - log("call event", registration.event, "for invocation id", invocationId) + util.wrapParentPromise ipc, registration.eventId, (invocationId) -> + debug("call event", registration.event, "for invocation id", invocationId) ids = { - callbackId: registration.callbackId + eventId: registration.eventId invocationId: invocationId } ipc.send("execute", registration.event, ids, args) @@ -66,7 +67,7 @@ module.exports = { pluginsProcess = null handleError = (err) -> - log("plugins process error:", err.stack) + debug("plugins process error:", err.stack) killPluginsProcess() err = errors.get("PLUGINS_ERROR", err.annotated or err.stack or err.message) err.title = "Error running plugin" @@ -84,7 +85,7 @@ module.exports = { !!registeredEvents[event] execute: (event, args...) -> - log("execute plugin event '#{event}' with args: %s", args...) + debug("execute plugin event '#{event}' with args: %o %o %o", args...) registeredEvents[event](args...) ## for testing purposes diff --git a/packages/server/lib/plugins/preprocessor.coffee b/packages/server/lib/plugins/preprocessor.coffee index 0fe18e1db047..5aceaa957380 100644 --- a/packages/server/lib/plugins/preprocessor.coffee +++ b/packages/server/lib/plugins/preprocessor.coffee @@ -21,7 +21,7 @@ errorMessage = (err = {}) -> .replace(/From previous event:\n?/g, "") clientSideError = (err) -> - console.error(pe.render(err)) + console.log(pe.render(err)) err = errorMessage(err) ## \n doesn't come through properly so preserve it so the ## runner can do the right thing @@ -62,9 +62,9 @@ plugins.registerHandler (ipc) -> ipc.send("preprocessor:close", filePath) module.exports = { + errorMessage + clientSideError emitter: baseEmitter - errorMessage: errorMessage - clientSideError: clientSideError getFile: (filePath, config, options = {}) -> filePath = path.join(config.projectRoot, filePath) diff --git a/packages/server/lib/plugins/util.coffee b/packages/server/lib/plugins/util.coffee index 93b2ef77ad14..f72ecf5d44ca 100644 --- a/packages/server/lib/plugins/util.coffee +++ b/packages/server/lib/plugins/util.coffee @@ -1,7 +1,9 @@ -EE = require("events") _ = require("lodash") +EE = require("events") +debug = require("debug")("cypress:server:plugins") Promise = require("bluebird") -log = require("debug")("cypress:server:plugins") + +UNDEFINED_SERIALIZED = "__cypress_undefined__" serializeError = (err) -> _.pick(err, "name", "message", "stack", "code", "annotated") @@ -30,24 +32,33 @@ module.exports = { wrapChildPromise: (ipc, invoke, ids, args = []) -> Promise.try -> - return invoke(ids.callbackId, args) + return invoke(ids.eventId, args) .then (value) -> + ## undefined is coerced into null when sent over ipc, but we need + ## to differentiate between them for 'task' event + if value is undefined + value = UNDEFINED_SERIALIZED ipc.send("promise:fulfilled:#{ids.invocationId}", null, value) .catch (err) -> ipc.send("promise:fulfilled:#{ids.invocationId}", serializeError(err)) - wrapParentPromise: (ipc, callbackId, callback) -> + wrapParentPromise: (ipc, eventId, callback) -> invocationId = _.uniqueId("inv") new Promise (resolve, reject) -> handler = (err, value) -> ipc.removeListener("promise:fulfilled:#{invocationId}", handler) + if err - log("promise rejected for id", invocationId, ":", err) + debug("promise rejected for id %s %o", invocationId, ":", err.stack) reject(_.extend(new Error(err.message), err)) return - log("promise resolved for id '#{invocationId}' with value", value) + if value is UNDEFINED_SERIALIZED + value = undefined + + debug("promise resolved for id '#{invocationId}' with value", value) + resolve(value) ipc.on("promise:fulfilled:#{invocationId}", handler) diff --git a/packages/server/lib/project.coffee b/packages/server/lib/project.coffee index 48cec002e678..b28f2e5b48e5 100644 --- a/packages/server/lib/project.coffee +++ b/packages/server/lib/project.coffee @@ -1,15 +1,14 @@ _ = require("lodash") R = require("ramda") -fs = require("fs-extra") EE = require("events") path = require("path") -glob = require("glob") Promise = require("bluebird") commitInfo = require("@cypress/commit-info") la = require("lazy-ass") check = require("check-more-types") +scaffoldLog = require("debug")("cypress:server:scaffold") +debug = require("debug")("cypress:server:project") cwd = require("./cwd") -ids = require("./ids") api = require("./api") user = require("./user") cache = require("./cache") @@ -17,21 +16,17 @@ config = require("./config") logger = require("./logger") errors = require("./errors") Server = require("./server") +plugins = require("./plugins") scaffold = require("./scaffold") Watchers = require("./watchers") Reporter = require("./reporter") +browsers = require("./browsers") savedState = require("./saved_state") Automation = require("./automation") -files = require("./controllers/files") -plugins = require("./plugins") preprocessor = require("./plugins/preprocessor") +fs = require("./util/fs") settings = require("./util/settings") -browsers = require("./browsers") -scaffoldLog = require("debug")("cypress:server:scaffold") -debug = require("debug")("cypress:server:project") - -fs = Promise.promisifyAll(fs) -glob = Promise.promisify(glob) +specsUtil = require("./util/specs") localCwd = cwd() @@ -189,7 +184,7 @@ class Project extends EE return if not found debug("watch plugins file") - @watchers.watch(cfg.pluginsFile, { + @watchers.watchTree(cfg.pluginsFile, { onChange: => ## TODO: completely re-open project instead? debug("plugins file changed") @@ -204,6 +199,8 @@ class Project extends EE ## watch anything return if not onSettingsChanged + debug("watch settings files") + obj = { onChange: (filePath, stats) => ## dont fire change events if we generated @@ -217,18 +214,32 @@ class Project extends EE } @watchers.watch(settings.pathToCypressJson(@projectRoot), obj) + @watchers.watch(settings.pathToCypressEnvJson(@projectRoot), obj) watchSettingsAndStartWebsockets: (options = {}, cfg = {}) -> @watchSettings(options.onSettingsChanged) + { reporter, projectRoot } = cfg + ## if we've passed down reporter ## then record these via mocha reporter if cfg.report - if not Reporter.isValidReporterName(cfg.reporter, cfg.projectRoot) - paths = Reporter.getSearchPathsForReporter(cfg.reporter, cfg.projectRoot) - errors.throw("INVALID_REPORTER_NAME", cfg.reporter, paths) + try + Reporter.loadReporter(reporter, projectRoot) + catch err + paths = Reporter.getSearchPathsForReporter(reporter, projectRoot) + + ## only include the message if this is the standard MODULE_NOT_FOUND + ## else include the whole stack + errorMsg = if err.code is "MODULE_NOT_FOUND" then err.message else err.stack + + errors.throw("INVALID_REPORTER_NAME", { + paths + error: errorMsg + name: reporter + }) - reporter = Reporter.create(cfg.reporter, cfg.reporterOptions, cfg.projectRoot) + reporter = Reporter.create(reporter, cfg.reporterOptions, projectRoot) @automation = Automation.create(cfg.namespace, cfg.socketIoCookie, cfg.screenshotsFolder) @@ -289,7 +300,9 @@ class Project extends EE setNewProject = (cfg) => ## decide if new project by asking scaffold ## and looking at previously saved user state - throw new Error("Missing integration folder") if not cfg.integrationFolder + if not cfg.integrationFolder + throw new Error("Missing integration folder") + @determineIsNewProject(cfg.integrationFolder) .then (untouchedScaffold) -> userHasSeenOnBoarding = _.get(cfg, 'state.showedOnBoardingModal', false) @@ -298,11 +311,11 @@ class Project extends EE .return(cfg) if c = @cfg - Promise.resolve(c) - else - config.get(@projectRoot, options) - .then (cfg) => @_setSavedState(cfg) - .then(setNewProject) + return Promise.resolve(c) + + config.get(@projectRoot, options) + .then (cfg) => @_setSavedState(cfg) + .then(setNewProject) # forces saving of project's state by first merging with argument saveState: (stateChanges = {}) -> @@ -323,37 +336,26 @@ class Project extends EE cfg.state = state cfg - ensureSpecUrl: (spec) -> + getSpecUrl: (spec) -> @getConfig() .then (cfg) => ## if we dont have a spec or its __all if not spec or (spec is "__all") - @getUrlBySpec(cfg.browserUrl, "/__all") + @normalizeSpecUrl(cfg.browserUrl, "/__all") else - @ensureSpecExists(spec) - .then (pathToSpec) => - ## TODO: - ## to handle both unit + integration tests we need - ## to figure out (based on the config) where this spec - ## lives. does it live in the integrationFolder or - ## the unit folder? - ## once we determine that we can then prefix it correctly - ## with either integration or unit - prefixedPath = @getPrefixedPathToSpec(cfg.integrationFolder, pathToSpec) - @getUrlBySpec(cfg.browserUrl, prefixedPath) - - ensureSpecExists: (spec) -> - specFile = path.resolve(@projectRoot, spec) - - ## we want to make it easy on the user by allowing them to pass both - ## an absolute path to the spec, or a relative path from their test folder - fs - .statAsync(specFile) - .return(specFile) - .catch -> - errors.throw("SPEC_FILE_NOT_FOUND", specFile) + ## TODO: + ## to handle both unit + integration tests we need + ## to figure out (based on the config) where this spec + ## lives. does it live in the integrationFolder or + ## the unit folder? + ## once we determine that we can then prefix it correctly + ## with either integration or unit + prefixedPath = @getPrefixedPathToSpec(cfg, spec) + @normalizeSpecUrl(cfg.browserUrl, prefixedPath) + + getPrefixedPathToSpec: (cfg, pathToSpec, type = "integration") -> + { integrationFolder, projectRoot } = cfg - getPrefixedPathToSpec: (integrationFolder, pathToSpec, type = "integration") -> ## for now hard code the 'type' as integration ## but in the future accept something different here @@ -364,13 +366,20 @@ class Project extends EE ## /Users/bmann/Dev/cypress-app/.projects/cypress/integration/foo.coffee ## ## becomes /integration/foo.coffee - "/" + path.join(type, path.relative(integrationFolder, pathToSpec)) + "/" + path.join(type, path.relative( + integrationFolder, + path.resolve(projectRoot, pathToSpec) + )) - getUrlBySpec: (browserUrl, specUrl) -> + normalizeSpecUrl: (browserUrl, specUrl) -> replacer = (match, p1) -> match.replace("//", "/") - [browserUrl, "#/tests", specUrl].join("/").replace(multipleForwardSlashesRe, replacer) + [ + browserUrl, + "#/tests", + specUrl + ].join("/").replace(multipleForwardSlashesRe, replacer) scaffold: (cfg) -> debug("scaffolding project %s", @projectRoot) @@ -398,7 +407,7 @@ class Project extends EE Promise.all(scaffolds) writeProjectId: (id) -> - attrs = {projectId: id} + attrs = { projectId: id } logger.info "Writing Project ID", _.clone(attrs) @generatedProjectIdTimestamp = new Date @@ -410,10 +419,7 @@ class Project extends EE getProjectId: -> @verifyExistence() .then => - if id = process.env.CYPRESS_PROJECT_ID - {projectId: id} - else - settings.read(@projectRoot) + settings.read(@projectRoot) .then (settings) => if settings and id = settings.projectId return id @@ -456,14 +462,14 @@ class Project extends EE api.getOrgs(authToken) @paths = -> - cache.getProjectPaths() + cache.getProjectRoots() @getPathsAndIds = -> - cache.getProjectPaths() - .map (projectPath) -> + cache.getProjectRoots() + .map (projectRoot) -> Promise.props({ - path: projectPath - id: settings.id(projectPath) + path: projectRoot + id: settings.id(projectRoot) }) @_mergeDetails = (clientProject, project) -> @@ -539,14 +545,6 @@ class Project extends EE .catch -> {path} - @removeIds = (p) -> - Project(p) - .verifyExistence() - .call("getConfig") - .then (cfg) -> - ## remove all of the ids for the test files found in the integrationFolder - ids.remove(cfg.integrationFolder) - @id = (path) -> Project(path).getProjectId() @@ -579,9 +577,9 @@ class Project extends EE # Given a path to the project, finds all specs # returns list of specs with respect to the project root - @findSpecs = (projectPath, specPattern) -> - debug("finding specs for project %s", projectPath) - la(check.unemptyString(projectPath), "missing project path", projectPath) + @findSpecs = (projectRoot, specPattern) -> + debug("finding specs for project %s", projectRoot) + la(check.unemptyString(projectRoot), "missing project path", projectRoot) la(check.maybe.unemptyString(specPattern), "invalid spec pattern", specPattern) ## if we have a spec pattern @@ -589,13 +587,13 @@ class Project extends EE ## then normalize to create an absolute ## file path from projectRoot ## ie: **/* turns into /Users/bmann/dev/project/**/* - specPattern = path.resolve(projectPath, specPattern) + specPattern = path.resolve(projectRoot, specPattern) - Project(projectPath) + Project(projectRoot) .getConfig() # TODO: handle wild card pattern or spec filename .then (cfg) -> - files.getTestFiles(cfg, specPattern) + specsUtil.find(cfg, specPattern) .then R.prop("integration") .then R.map(R.prop("name")) diff --git a/packages/server/lib/reporter.coffee b/packages/server/lib/reporter.coffee index ff2bd9c09d42..9ff6ab84caa2 100644 --- a/packages/server/lib/reporter.coffee +++ b/packages/server/lib/reporter.coffee @@ -26,6 +26,18 @@ Mocha.Suite.prototype.titlePath = -> Mocha.Runnable.prototype.titlePath = -> @parent.titlePath().concat([@title]) +getParentTitle = (runnable, titles) -> + if not titles + titles = [runnable.title] + + if p = runnable.parent + if t = p.title + titles.unshift(t) + + getParentTitle(p, titles) + else + titles + createSuite = (obj, parent) -> suite = new Mocha.Suite(obj.title, {}) suite.parent = parent if parent @@ -46,50 +58,73 @@ createRunnable = (obj, parent) -> runnable.async = obj.async runnable.sync = obj.sync runnable.duration = obj.duration - runnable.state = obj.state + runnable.state = obj.state ? "skipped" ## skipped by default runnable.body ?= body runnable.parent = parent if parent return runnable -mergeRunnable = (testProps, runnables) -> - runnable = runnables[testProps.id] +mergeRunnable = (eventName) -> + return (testProps, runnables) -> + runnable = runnables[testProps.id] + + _.extend(runnable, testProps) + +safelyMergeRunnable = (hookProps, runnables) -> + { hookId, title, hookName, body, type } = hookProps + + if not runnable = runnables[hookId] + runnables[hookId] = { + hookId + type + title + body + hookName + } + + _.extend({}, runnables[hookProps.id], hookProps) + +mergeErr = (runnable, runnables, stats) -> + ## this will always be a test because + ## we reset hook id's to match tests + test = runnables[runnable.id] + test.err = runnable.err + test.state = "failed" + test.duration ?= test.duration + + if runnable.type is "hook" + test.failedFromHookId = runnable.hookId - if not runnable.started - testProps.started = Date.now() + ## dont mutate the test, and merge in the runnable title + ## in the case its a hook so that we emit the right 'fail' + ## event for reporters + test = _.extend({}, test, { title: runnable.title }) - _.extend(runnable, testProps) + [test, test.err] -safelyMergeRunnable = (testProps, runnables) -> - _.extend({}, runnables[testProps.id], testProps) +setDate = (obj, runnables, stats) -> + if s = obj.start + stats.wallClockStartedAt = new Date(s) -mergeErr = (test, runnables, runner) -> - ## increment runner failures - ## because thats what the fail() fn does. - ## useful for reporters expecting this - ## and for 'end' event fn callbacks - runner.failures += 1 + if e = obj.end + stats.wallClockEndedAt = new Date(e) - runnable = runnables[test.id] - runnable.err = test.err - runnable.state = "failed" - runnable.duration ?= test.duration - runnable = _.extend({}, runnable, {title: test.title}) - [runnable, test.err] + return null events = { - "start": true - "end": true - "suite": mergeRunnable - "suite end": mergeRunnable - "test": mergeRunnable - "test end": mergeRunnable + "start": setDate + "end": setDate + "suite": mergeRunnable("suite") + "suite end": mergeRunnable("suite end") + "test": mergeRunnable("test") + "test end": mergeRunnable("test end") "hook": safelyMergeRunnable "hook end": safelyMergeRunnable - "pass": mergeRunnable - "pending": mergeRunnable + "pass": mergeRunnable("pass") + "pending": mergeRunnable("pending") "fail": mergeErr + "test:after:run": mergeRunnable("test:after:run") ## our own custom event } reporters = { @@ -107,13 +142,17 @@ class Reporter @reporterOptions = reporterOptions setRunnables: (rootRunnable = {}) -> + ## manage stats ourselves + @stats = { suites: 0, tests: 0, passes: 0, pending: 0, skipped: 0, failures: 0 } @runnables = {} rootRunnable = @_createRunnable(rootRunnable, "suite") reporter = Reporter.loadReporter(@reporterName, @projectRoot) @mocha = new Mocha({reporter: reporter}) @mocha.suite = rootRunnable @runner = new Mocha.Runner(rootRunnable) - @reporter = new @mocha._reporter(@runner, {reporterOptions: @reporterOptions}) + @reporter = new @mocha._reporter(@runner, { + reporterOptions: @reporterOptions + }) @runner.ignoreLeaks = true @@ -131,6 +170,8 @@ class Reporter else throw new Error("Unknown runnable type: '#{type}'") + runnable.id = runnableProps.id + @runnables[runnableProps.id] = runnable return runnable @@ -145,34 +186,46 @@ class Reporter debug("got mocha event '%s' with args: %o", event, args) ## transform the arguments if ## there is an event.fn callback - args = e.apply(@, args.concat(@runnables, @runner)) + args = e.apply(@, args.concat(@runnables, @stats)) [event].concat(args) - normalize: (test = {}) -> - getParentTitle = (runnable, titles) -> - if p = runnable.parent - if t = p.title - titles.unshift(t) - - getParentTitle(p, titles) - else - titles - - err = test.err ? {} - - titles = [test.title] - - ## TODO: move the separator into some shared util function somewhere + normalizeHook: (hook = {}) -> + { + hookId: hook.hookId + hookName: hook.hookName + title: getParentTitle(hook) + body: hook.body + } + normalizeTest: (test = {}) -> + get = (prop) -> + _.get(test, prop, null) + + ## use this or null + if wcs = get("wallClockStartedAt") + ## convert to actual date object + wcs = new Date(wcs) + + ## wallClockDuration: + ## this is the 'real' duration of wall clock time that the + ## user 'felt' when the test run. it includes everything + ## from hooks, to the test itself, to lifecycle, and event + ## async browser compute time. this number is likely higher + ## than summing the durations of the timings. + ## { - clientId: test.id - title: getParentTitle(test, titles).join(" /// ") - duration: test.duration - stack: err.stack - error: err.message - started: test.started - # videoTimestamp: test.started - videoStart + testId: get("id") + title: getParentTitle(test) + state: get("state") + body: get("body") + stack: get("err.stack") + error: get("err.message") + timings: get("timings") + failedFromHookId: get("failedFromHookId") + wallClockStartedAt: wcs + wallClockDuration: get("wallClockDuration") + videoTimestamp: null ## always start this as null } end: -> @@ -182,31 +235,71 @@ class Reporter new Promise (resolve, reject) => @reporter.done(failures, resolve) .then => - @stats() + @results() else - @stats() + @results() + + results: -> + tests = _ + .chain(@runnables) + .filter({type: "test"}) + .map(@normalizeTest) + .value() + + hooks = _ + .chain(@runnables) + .filter({type: "hook"}) + .map(@normalizeHook) + .value() - stats: -> - failingTests = _ + suites = _ .chain(@runnables) - .filter({state: "failed"}) - .map(@normalize) + .filter({root: false}) ## don't include root suite .value() - stats = @runner.stats + ## default to 0 + @stats.wallClockDuration = 0 + + { wallClockStartedAt, wallClockEndedAt } = @stats + + if wallClockStartedAt and wallClockEndedAt + @stats.wallClockDuration = wallClockEndedAt - wallClockStartedAt + + @stats.suites = suites.length + @stats.tests = tests.length + @stats.passes = _.filter(tests, { state: "passed" }).length + @stats.pending = _.filter(tests, { state: "pending" }).length + @stats.skipped = _.filter(tests, { state: "skipped" }).length + @stats.failures = _.filter(tests, { state: "failed" }).length + + ## return an object of results + return { + ## this is our own stats object + stats: @stats + + reporter: @reporterName - _.extend {reporter: @reporterName, failingTests: failingTests}, _.pick(stats, STATS) + ## this comes from the reporter, not us + reporterStats: @runner.stats + + hooks + + tests + } @setVideoTimestamp = (videoStart, tests = []) -> _.map tests, (test) -> - test.videoTimestamp = test.started - videoStart + ## if we have a wallClockStartedAt + if wcs = test.wallClockStartedAt + test.videoTimestamp = test.wallClockStartedAt - videoStart test @create = (reporterName, reporterOptions, projectRoot) -> new Reporter(reporterName, reporterOptions, projectRoot) @loadReporter = (reporterName, projectRoot) -> - debug("loading reporter #{reporterName}") + debug("trying to load reporter:", reporterName) + if r = reporters[reporterName] debug("#{reporterName} is built-in reporter") return require(r) @@ -219,22 +312,28 @@ class Reporter ## that is local (./custom-reporter.js) ## or one installed by the user through npm try + p = path.resolve(projectRoot, reporterName) + ## try local - debug("loading local reporter by name #{reporterName}") + debug("trying to require local reporter with path:", p) ## using path.resolve() here so we can just pass an ## absolute path as the reporterName which avoids ## joining projectRoot unnecessarily - return require(path.resolve(projectRoot, reporterName)) + return require(p) catch err + if err.code isnt "MODULE_NOT_FOUND" + ## bail early if the error wasn't MODULE_NOT_FOUND + ## because that means theres something actually wrong + ## with the found reporter + throw err + + p = path.resolve(projectRoot, "node_modules", reporterName) + ## try npm. if this fails, we're out of options, so let it throw - debug("loading NPM reporter module #{reporterName} from #{projectRoot}") + debug("trying to require local reporter with path:", p) - try - return require(path.resolve(projectRoot, "node_modules", reporterName)) - catch err - msg = "Could not find reporter module #{reporterName} relative to #{projectRoot}" - throw new Error(msg) + return require(p) @getSearchPathsForReporter = (reporterName, projectRoot) -> _.uniq([ @@ -242,12 +341,4 @@ class Reporter path.resolve(projectRoot, "node_modules", reporterName) ]) - @isValidReporterName = (reporterName, projectRoot) -> - try - Reporter.loadReporter(reporterName, projectRoot) - debug("reporter #{reporterName} is valid name") - true - catch - false - module.exports = Reporter diff --git a/packages/server/lib/saved_state.coffee b/packages/server/lib/saved_state.coffee index 8faffe8c3dab..a1e7ae9f227c 100644 --- a/packages/server/lib/saved_state.coffee +++ b/packages/server/lib/saved_state.coffee @@ -21,8 +21,8 @@ stateFiles = {} # state should have width = 200 # async promise-returning function -findSavedSate = (projectPath) -> - savedStateUtil.formStatePath(projectPath) +findSavedSate = (projectRoot) -> + savedStateUtil.formStatePath(projectRoot) .then (statePath) -> fullStatePath = appData.projectsPath(statePath) log('full state path %s', fullStatePath) diff --git a/packages/server/lib/scaffold.coffee b/packages/server/lib/scaffold.coffee index c2f8490599d9..b0d895ed6ea3 100644 --- a/packages/server/lib/scaffold.coffee +++ b/packages/server/lib/scaffold.coffee @@ -1,103 +1,111 @@ _ = require("lodash") -fs = require("fs-extra") Promise = require("bluebird") path = require("path") cypressEx = require("@packages/example") -glob = require("glob") -cwd = require("./cwd") log = require("debug")("cypress:server:scaffold") -{ propEq, complement, equals, compose, head, isEmpty, always } = require("ramda") +fs = require("./util/fs") +glob = require("./util/glob") +cwd = require("./cwd") +debug = require("debug")("cypress:server:scaffold") +{ equals, head, isEmpty, always } = require("ramda") { isDefault } = require("./util/config") -glob = Promise.promisify(glob) -fs = Promise.promisifyAll(fs) - -INTEGRATION_EXAMPLE_SPEC = cypressEx.getPathToExample() -INTEGRATION_EXAMPLE_NAME = path.basename(INTEGRATION_EXAMPLE_SPEC) - -## we are assuming example spec is a single file for now -numberOfExampleSpecs = 1 - -# a few utility functions for quickly comparing list of files -# to number of expected example files -isOneFile = propEq('length', numberOfExampleSpecs) -isNotOneFile = complement(isOneFile) -isFileNameDefault = compose( - equals(INTEGRATION_EXAMPLE_NAME), - path.basename -) - -# TODO why isn't R.complement(isFileNameDefault) working?! -isFileNameChanged = (filename) -> !isFileNameDefault(filename) - -integrationExampleSize = -> - fs - .statAsync(INTEGRATION_EXAMPLE_SPEC) - .get("size") +getPathFromIntegrationFolder = (file) -> + file.substring(file.indexOf("integration/") + "integration/".length) + +exampleSpecsFullPaths = cypressEx.getPathToExamples() +exampleFolderName = cypressEx.getFolderName() +## short paths relative to integration folder (i.e. examples/actions.spec.js) +exampleSpecs = _.map exampleSpecsFullPaths, (file) -> + getPathFromIntegrationFolder(file) + +isDifferentNumberOfFiles = (files) -> + files.length isnt exampleSpecs.length + +## index for quick lookup and for getting full path from short path +exampleSpecsIndex = _.transform(exampleSpecs, (memo, spec, i) -> + memo[spec] = exampleSpecsFullPaths[i] +, {}) +getIndexedExample = (file) -> + exampleSpecsIndex[getPathFromIntegrationFolder(file)] + +filesNamesAreDifferent = (files) -> + _.some files, (file) -> + not getIndexedExample(file) + +getFileSize = (file) -> + fs.statAsync(file).get("size") + +filesSizesAreSame = (files) -> + Promise.join( + Promise.all(_.map(files, getFileSize)), + Promise.all(_.map(files, (file) -> getFileSize(getIndexedExample(file)))) + ) + .spread (fileSizes, originalFileSizes) -> + _.every fileSizes, (size, i) -> + size is originalFileSizes[i] isNewProject = (integrationFolder) -> ## logic to determine if new project ## 1. there are no files in 'integrationFolder' - ## 2. there is only 1 file in 'integrationFolder' - ## 3. the file is called 'example_spec.js' - ## 4. the bytes of the file match lib/scaffold/example_spec.js - getCurrentSize = (file) -> - fs - .statAsync(file) - .get("size") - - glob("**/*", { cwd: integrationFolder, realpath: true }) + ## 2. there is a different number of files in 'integrationFolder' + ## 3. the files are named the same as the example files + ## 4. the bytes of the files match the example files + + glob("**/*", { cwd: integrationFolder, realpath: true, nodir: true }) .then (files) -> - log "found #{files.length} files in folder #{integrationFolder}" + debug("found #{files.length} files in folder #{integrationFolder}") + debug("determine if we should scaffold:") + ## TODO: add tests for this + debug("- empty?", isEmpty(files)) return true if isEmpty(files) # 1 - return false if isNotOneFile(files) # 2 + debug("- different number of files?", isDifferentNumberOfFiles(files)) + return false if isDifferentNumberOfFiles(files) # 2 - exampleSpec = head(files) - log "Checking spec filename if default #{exampleSpec}" + filesNamesDifferent = filesNamesAreDifferent(files) + debug("- different file names?", filesNamesDifferent) + return false if filesNamesDifferent # 3 - return false if isFileNameChanged(exampleSpec) # 3 - - log "Checking spec file size #{exampleSpec}" - # 4 - Promise.join( - getCurrentSize(exampleSpec), - integrationExampleSize(), - equals - ) + filesSizesAreSame(files).then (sameSizes) -> + debug("- same sizes?", sameSizes) + sameSizes module.exports = { isNewProject + integrationExampleName: -> exampleFolderName + integration: (folder, config) -> - log("integration folder #{folder}") + debug("integration folder #{folder}") ## skip if user has explicitly set integrationFolder return Promise.resolve() if not isDefault(config, "integrationFolder") @verifyScaffolding folder, => - log("copying examples into #{folder}") - @_copy(INTEGRATION_EXAMPLE_SPEC, folder, config) + debug("copying examples into #{folder}") + Promise.all _.map exampleSpecsFullPaths, (file) => + @_copy(file, path.join(folder, exampleFolderName), config) fixture: (folder, config) -> - log("fixture folder #{folder}") + debug("fixture folder #{folder}") ## skip if user has explicitly set fixturesFolder return Promise.resolve() if not config.fixturesFolder or not isDefault(config, "fixturesFolder") @verifyScaffolding folder, => - log("copying example.json into #{folder}") + debug("copying example.json into #{folder}") @_copy("fixtures/example.json", folder, config) support: (folder, config) -> - log("support folder #{folder}, support file #{config.supportFile}") + debug("support folder #{folder}, support file #{config.supportFile}") ## skip if user has explicitly set supportFile return Promise.resolve() if not config.supportFile or not isDefault(config, "supportFile") @verifyScaffolding(folder, => - log("copying commands.js and index.js to #{folder}") + debug("copying commands.js and index.js to #{folder}") Promise.join( @_copy("support/commands.js", folder, config) @_copy("support/index.js", folder, config) @@ -105,14 +113,14 @@ module.exports = { ) plugins: (folder, config) -> - log("plugins folder #{folder}") + debug("plugins folder #{folder}") ## skip if user has explicitly set pluginsFile if not config.pluginsFile or not isDefault(config, "pluginsFile") return Promise.resolve() @verifyScaffolding folder, => - log("copying index.js into #{folder}") + debug("copying index.js into #{folder}") @_copy("plugins/index.js", folder, config) _copy: (file, folder, config) -> @@ -124,10 +132,6 @@ module.exports = { fs.copyAsync(src, dest) - integrationExampleSize - - integrationExampleName: always(INTEGRATION_EXAMPLE_NAME) - verifyScaffolding: (folder, fn) -> ## we want to build out the folder + and example files ## but only create the example files if the folder doesn't @@ -140,13 +144,13 @@ module.exports = { ## this is ideal because users who are upgrading to newer cypress version ## will still get the files scaffolded but existing users won't be ## annoyed by new example files coming into their projects unnecessarily - # console.log('-- verify', folder) - log "verify scaffolding in #{folder}" + # console.debug('-- verify', folder) + debug("verify scaffolding in #{folder}") fs.statAsync(folder) .then -> - log("folder #{folder} already exists") + debug("folder #{folder} already exists") .catch => - log("missing folder #{folder}") + debug("missing folder #{folder}") fn.call(@) fileTree: (config = {}) -> @@ -157,9 +161,8 @@ module.exports = { getFilePath = (dir, name) -> path.relative(config.projectRoot, path.join(dir, name)) - files = [ - getFilePath(config.integrationFolder, "example_spec.js") - ] + files = _.map exampleSpecs, (file) -> + getFilePath(config.integrationFolder, file) if config.fixturesFolder files = files.concat([ @@ -177,7 +180,7 @@ module.exports = { getFilePath(path.dirname(config.pluginsFile), "index.js") ]) - log("scaffolded files %j", files) + debug("scaffolded files %j", files) return @_fileListToTree(files) diff --git a/packages/server/lib/screenshots.coffee b/packages/server/lib/screenshots.coffee index 67dff82165fb..5559a8ac648f 100644 --- a/packages/server/lib/screenshots.coffee +++ b/packages/server/lib/screenshots.coffee @@ -1,14 +1,14 @@ -fs = require("fs-extra") +_ = require("lodash") mime = require("mime") path = require("path") -glob = require("glob") bytes = require("bytes") -sizeOf = require("image-size") Promise = require("bluebird") dataUriToBuffer = require("data-uri-to-buffer") - -fs = Promise.promisifyAll(fs) -glob = Promise.promisify(glob) +Jimp = require("jimp") +sizeOf = require("image-size") +debug = require("debug")("cypress:server:screenshot") +fs = require("./util/fs") +glob = require("./util/glob") RUNNABLE_SEPARATOR = " -- " invalidCharsRe = /[^0-9a-zA-Z-_\s]/g @@ -18,7 +18,166 @@ invalidCharsRe = /[^0-9a-zA-Z-_\s]/g ## screenshots since its possible two screenshots with ## the same name will be written to the file system +Jimp.prototype.getBuffer = Promise.promisify(Jimp.prototype.getBuffer) + +isBlack = (rgba) -> + "#{rgba.r}#{rgba.g}#{rgba.b}" is "000" + +isWhite = (rgba) -> + "#{rgba.r}#{rgba.g}#{rgba.b}" is "255255255" + +## when we hide the runner UI for an app or fullPage capture +## the browser doesn't paint synchronously, it can take 100+ ms +## to ensure that the runner UI has been hidden, we put +## pixels in the corners of the runner UI like so: +## +## ------------- +## |g w w| w = white +## |w | g = grey +## | | b = black +## |w b| +## ------------- +## +## when taking an 'app' or 'fullPage' capture, we ensure that the pixels +## are NOT there before accepting the screenshot +## when taking a 'runner' capture, we ensure the pixels ARE there + +hasHelperPixels = (image) -> + topLeft = Jimp.intToRGBA(image.getPixelColor(0, 0)) + topLeftRight = Jimp.intToRGBA(image.getPixelColor(1, 0)) + topLeftDown = Jimp.intToRGBA(image.getPixelColor(0, 1)) + topRight = Jimp.intToRGBA(image.getPixelColor(image.bitmap.width, 0)) + bottomLeft = Jimp.intToRGBA(image.getPixelColor(0, image.bitmap.height)) + bottomRight = Jimp.intToRGBA(image.getPixelColor(image.bitmap.width, image.bitmap.height)) + + return ( + (not isWhite(topLeft)) and + isWhite(topLeftRight) and + isWhite(topLeftDown) and + isWhite(topRight) and + isBlack(bottomRight) and + isWhite(bottomLeft) + ) + +## if somehow after 10 tries, the condition isn't met, just +## accept the current screenshot +MAX_TRIES = 10 + +captureAndCheck = (data, automate, condition) -> + tries = 0 + do attempt = -> + tries++ + debug("capture and check attempt ##{tries}") + + takenAt = new Date().toJSON() + + automate(data) + .then (dataUrl) -> + debug("received screenshot data from automation layer") + + Jimp.read(dataUriToBuffer(dataUrl)) + .then (image) -> + debug("read buffer to image #{image.bitmap.width} x #{image.bitmap.height}") + + if tries >= MAX_TRIES or condition(data, image) + return { image, takenAt } + else + attempt() + +isAppOnly = (data) -> + data.capture is "viewport" or data.capture is "fullPage" + +isMultipart = (data) -> + _.isNumber(data.current) and _.isNumber(data.total) + +crop = (image, dimensions, pixelRatio = 1) -> + dimensions = _.transform dimensions, (result, value, dimension) -> + result[dimension] = value * pixelRatio + + x = Math.min(dimensions.x, image.bitmap.width - 1) + y = Math.min(dimensions.y, image.bitmap.height - 1) + width = Math.min(dimensions.width, image.bitmap.width - x) + height = Math.min(dimensions.height, image.bitmap.height - y) + debug("crop: from #{image.bitmap.width} x #{image.bitmap.height}") + debug(" to #{width} x #{height} at (#{x}, #{y})") + + image.crop(x, y, width, height) + +pixelCondition = (data, image) -> + hasPixels = hasHelperPixels(image) + return ( + (isAppOnly(data) and not hasPixels) or + (not isAppOnly(data) and hasPixels) + ) + +multipartImages = [] + +clearMultipartState = -> + multipartImages = [] + +compareLast = (data, image) -> + ## ensure the previous image isn't the same, which might indicate the + ## page has not scrolled yet + previous = _.last(multipartImages) + if not previous + return true + debug("compare", previous.image.hash(), "vs", image.hash()) + return previous.image.hash() isnt image.hash() + +multipartCondition = (data, image) -> + if data.current is 1 + pixelCondition(data, image) and compareLast(data, image) + else + compareLast(data, image) + +stitchScreenshots = (pixelRatio) -> + width = Math.min(_.map(multipartImages, "data.clip.width")...) + height = _.sumBy(multipartImages, "data.clip.height") + + debug("stitch #{multipartImages.length} images together") + + takenAts = [] + + fullImage = new Jimp(width, height) + heightMarker = 0 + _.each multipartImages, ({ data, image, takenAt }) -> + croppedImage = image.clone() + crop(croppedImage, data.clip, pixelRatio) + + debug("stitch: add image at (0, #{heightMarker})") + + takenAts.push(takenAt) + fullImage.composite(croppedImage, 0, heightMarker) + heightMarker += croppedImage.bitmap.height + + return { image: fullImage, takenAt: takenAts } + +isBuffer = (details) -> + !!details.buffer + +getType = (details) -> + if isBuffer(details) + details.buffer.type + else + details.image.getMIME() + +getBuffer = (details) -> + if isBuffer(details) + Promise.resolve(details.buffer) + else + details.image.getBuffer(Jimp.AUTO) + +getDimensions = (details) -> + if isBuffer(details) + sizeOf(details.buffer) + else + _.pick(details.image.bitmap, "width", "height") + module.exports = { + crop + + clearMultipartState + copy: (src, dest) -> fs .copyAsync(src, dest, {overwrite: true}) @@ -31,33 +190,86 @@ module.exports = { glob(screenshotsFolder, {nodir: true}) - save: (data, dataUrl, screenshotsFolder) -> - buffer = dataUriToBuffer(dataUrl) + capture: (data, automate) -> + ## for failure screenshots, we keep it simple to avoid latency + ## caused by jimp reading the image buffer + if data.simple + takenAt = new Date().toJSON() + return automate(data).then (dataUrl) -> + { + takenAt + multipart: false + buffer: dataUriToBuffer(dataUrl) + } - ## use the screenshots specific name or - ## simply make its name the result of the titles - name = data.name ? data.titles.join(RUNNABLE_SEPARATOR) + condition = if isMultipart(data) then multipartCondition else pixelCondition - ## strip out any invalid characters out of the name - name = name.replace(invalidCharsRe, "") + captureAndCheck(data, automate, condition) + .then ({ image, takenAt }) -> + pixelRatio = image.bitmap.width / data.viewport.width + multipart = isMultipart(data) + debug("pixel ratio is", pixelRatio) - ## join name + extension with '.' - name = [name, mime.extension(buffer.type)].join(".") + if multipart + debug("multi-part #{data.current}/#{data.total}") + + if multipart and data.total > 1 + ## keep previous screenshot partials around b/c if two screenshots are + ## taken in a row, the UI might not be caught up so we need something + ## to compare the new one to + ## only clear out once we're ready to save the first partial for the + ## screenshot currently being taken + if data.current is 1 + clearMultipartState() + + multipartImages.push({ data, image, takenAt }) + + if data.current is data.total + { image } = stitchScreenshots(pixelRatio) + return { image, pixelRatio, multipart, takenAt } + else + return {} + + if isAppOnly(data) or isMultipart(data) + crop(image, data.clip, pixelRatio) + + return { image, pixelRatio, multipart, takenAt } + .then ({ image, pixelRatio, multipart, takenAt }) -> + return null if not image + + if image and data.userClip + crop(image, data.userClip, pixelRatio) + + return { image, pixelRatio, multipart, takenAt } + + save: (data, details, screenshotsFolder) -> + type = getType(details) + + name = data.name ? data.titles.join(RUNNABLE_SEPARATOR) + name = name.replace(invalidCharsRe, "") + name = [name, mime.extension(type)].join(".") pathToScreenshot = path.join(screenshotsFolder, name) - fs.outputFileAsync(pathToScreenshot, buffer) + debug("save", pathToScreenshot) + + getBuffer(details) + .then (buffer) -> + fs.outputFileAsync(pathToScreenshot, buffer) .then -> - fs.statAsync(pathToScreenshot) - .get("size") + fs.statAsync(pathToScreenshot).get("size") .then (size) -> - dimensions = sizeOf(buffer) + dimensions = getDimensions(details) + + { multipart, pixelRatio, takenAt } = details { - size: bytes(size, {unitSeparator: " "}) - path: pathToScreenshot - width: dimensions.width - height: dimensions.height + takenAt + dimensions + multipart + pixelRatio + size: bytes(size, {unitSeparator: " "}) + path: pathToScreenshot } } diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee index 7f89c62c2d6a..83273a06a74b 100644 --- a/packages/server/lib/server.coffee +++ b/packages/server/lib/server.coffee @@ -530,6 +530,32 @@ class Server {protocol} = url.parse(remoteOrigin) {hostname} = url.parse("http://#{host}") + onProxyErr = (err, req, res) -> + ## by default http-proxy will call socket.end + ## with no data, so we need to override the end + ## function and write our own response + ## https://github.com/nodejitsu/node-http-proxy/blob/master/lib/http-proxy/passes/ws-incoming.js#L159 + end = socket.end + socket.end = -> + socket.end = end + + response = [ + "HTTP/#{req.httpVersion} 502 #{statusCode.getText(502)}" + "X-Cypress-Proxy-Error-Message: #{err.message}" + "X-Cypress-Proxy-Error-Code: #{err.code}" + ].join("\r\n") + "\r\n\r\n" + + proxiedUrl = "#{protocol}//#{hostname}:#{port}" + + debug( + "Got ERROR proxying websocket connection to url: '%s' received error: '%s' with code '%s'", + proxiedUrl, + err.toString() + err.code + ) + + socket.end(response) + proxy.ws(req, socket, head, { secure: false target: { @@ -537,7 +563,7 @@ class Server port: port protocol: protocol } - }) + }, onProxyErr) else ## we can't do anything with this socket ## since we don't know how to proxy it! diff --git a/packages/server/lib/socket.coffee b/packages/server/lib/socket.coffee index eee2c928c1a8..29311be92d93 100644 --- a/packages/server/lib/socket.coffee +++ b/packages/server/lib/socket.coffee @@ -1,13 +1,14 @@ _ = require("lodash") -fs = require("fs-extra") path = require("path") uuid = require("node-uuid") Promise = require("bluebird") socketIo = require("@packages/socket") +fs = require("./util/fs") open = require("./util/open") pathHelpers = require("./util/path_helpers") cwd = require("./cwd") exec = require("./exec") +task = require("./task") files = require("./files") fixture = require("./fixture") errors = require("./errors") @@ -304,6 +305,8 @@ class Socket files.writeFile(config.projectRoot, args[0], args[1], args[2]) when "exec" exec.run(config.projectRoot, args[0]) + when "task" + task.run(config.pluginsFile, args[0]) else throw new Error( "You requested a backend event we cannot handle: #{eventName}" diff --git a/packages/server/lib/stats.coffee b/packages/server/lib/stats.coffee deleted file mode 100644 index 24a52666bd16..000000000000 --- a/packages/server/lib/stats.coffee +++ /dev/null @@ -1,33 +0,0 @@ -_ = require("lodash") -chalk = require("chalk") - -TRANSLATION = { - tests: "Tests" - passes: "Passes" - failures: "Failures" - pending: "Pending" - duration: "Duration" - screenshots: "Screenshots" - video: "Video Recorded" - version: "Cypress Version" -} - -KEYS = _.keys(TRANSLATION) -LENS = _.map TRANSLATION, (val, key) -> val.length -MAX = Math.max(LENS...) - -module.exports = { - format: (color, val, key) -> - word = " - " + TRANSLATION[key] + ":" - - key = _.padEnd(word, MAX + 6) - - chalk.white(key) + chalk[color](val) - - display: (color, stats = {}) -> - stats = _.pick(stats, KEYS) - - _.each stats, (val, key) => - console.log(@format(color, val, key)) - -} diff --git a/packages/server/lib/stdout.coffee b/packages/server/lib/stdout.coffee deleted file mode 100644 index 76b59e2ef2f9..000000000000 --- a/packages/server/lib/stdout.coffee +++ /dev/null @@ -1,37 +0,0 @@ -_write = process.stdout.write -_log = process.log - -module.exports = { - capture: -> - logs = [] - - ## lazily backup write to enable - ## injection - write = process.stdout.write - log = process.log - - ## electron adds a new process.log - ## method for windows instead of process.stdout.write - ## https://github.com/cypress-io/cypress/issues/977 - if log - process.log = (str) -> - logs.push(str) - - log.apply(@, arguments) - - process.stdout.write = (str) -> - logs.push(str) - - write.apply(@, arguments) - - return { - toString: -> logs.join("") - - data: logs - } - - restore: -> - ## restore to the originals - process.stdout.write = _write - process.log = _log -} diff --git a/packages/server/lib/task.coffee b/packages/server/lib/task.coffee new file mode 100644 index 000000000000..e7600ebbf936 --- /dev/null +++ b/packages/server/lib/task.coffee @@ -0,0 +1,46 @@ +_ = require("lodash") +Promise = require("bluebird") +debug = require("debug")("cypress:server:task") +plugins = require("./plugins") + +docsUrl = "https://on.cypress.io/api/task" + +throwKnownError = (message, props = {}) -> + err = new Error(message) + _.extend(err, props, { isKnownError: true }) + throw err + +module.exports = { + run: (pluginsFilePath, options) -> + debug("run task", options.task, "with arg", options.arg) + + fileAndDocsUrl = "\n\nFix this in your plugins file here:\n#{pluginsFilePath}\n\n#{docsUrl}" + + Promise + .try -> + if not plugins.has("task") + debug("'task' event is not registered") + throwKnownError("The 'task' event has not been registered in the plugins file. You must register it before using cy.task()#{fileAndDocsUrl}") + + plugins.execute("task", options.task, options.arg) + .then (result) -> + if result is "__cypress_unhandled__" + debug("task is unhandled") + return plugins.execute("_get:task:keys").then (keys) -> + throwKnownError("The task '#{options.task}' was not handled in the plugins file. The following tasks are registered: #{keys.join(", ")}#{fileAndDocsUrl}") + + if result is undefined + debug("result is undefined") + return plugins.execute("_get:task:body", options.task).then (body) -> + throwKnownError("The task '#{options.task}' returned undefined. You must return a promise, a value, or null to indicate that the task was handled.\n\nThe task handler was:\n\n#{body}#{fileAndDocsUrl}") + + debug("result is:", result) + return result + .timeout(options.timeout) + .catch Promise.TimeoutError, -> + debug("timed out after #{options.timeout}ms") + plugins.execute("_get:task:body", options.task).then (body) -> + err = new Error("The task handler was:\n\n#{body}#{fileAndDocsUrl}") + err.timedOut = true + throw err +} diff --git a/packages/server/lib/updater.coffee b/packages/server/lib/updater.coffee index c045c7952a32..5c9f5a2cbf5f 100644 --- a/packages/server/lib/updater.coffee +++ b/packages/server/lib/updater.coffee @@ -1,5 +1,4 @@ _ = require("lodash") -fs = require("fs-extra") nmi = require("node-machine-id") debug = require("debug")("cypress:server:updater") semver = require("semver") diff --git a/packages/server/lib/upload.coffee b/packages/server/lib/upload.coffee index 31cac3e2ea05..97d73e9c071f 100644 --- a/packages/server/lib/upload.coffee +++ b/packages/server/lib/upload.coffee @@ -1,9 +1,7 @@ -fs = require("fs-extra") r = require("request") rp = require("request-promise") Promise = require("bluebird") - -fs = Promise.promisifyAll(fs) +fs = require("./util/fs") module.exports = { send: (pathToFile, url) -> diff --git a/packages/server/lib/util/app_data.coffee b/packages/server/lib/util/app_data.coffee index 9970b649f137..216e186c2075 100644 --- a/packages/server/lib/util/app_data.coffee +++ b/packages/server/lib/util/app_data.coffee @@ -1,4 +1,4 @@ -fs = require("fs-extra") +os = require("os") path = require("path") ospath = require("ospath") Promise = require("bluebird") @@ -7,9 +7,8 @@ check = require("check-more-types") log = require("debug")("cypress:server:appdata") pkg = require("@packages/root") cwd = require("../cwd") -os = require("os") +fs = require("../util/fs") -fs = Promise.promisifyAll(fs) name = pkg.productName or pkg.name data = ospath.data() @@ -17,9 +16,9 @@ if not name throw new Error("Root package is missing name") getSymlinkType = -> - if os.platform() == "win32" - "junction" - else + if os.platform() == "win32" + "junction" + else "dir" isProduction = -> diff --git a/packages/server/lib/util/args.coffee b/packages/server/lib/util/args.coffee index 162ab45bf6bf..6211eb5db4cb 100644 --- a/packages/server/lib/util/args.coffee +++ b/packages/server/lib/util/args.coffee @@ -9,7 +9,7 @@ nestedObjectsInCurlyBracesRe = /\{(.+?)\}/g nestedArraysInSquareBracketsRe = /\[(.+?)\]/g everythingAfterFirstEqualRe = /=(.+)/ -whitelist = "cwd appPath execPath apiKey smokeTest getKey generateKey runProject project spec reporter reporterOptions port env ci record updating ping key logs clearLogs returnPkg version mode removeIds headed config exit exitWithCode browser headless outputPath group groupId parallel parallelId".split(" ") +whitelist = "cwd appPath execPath apiKey smokeTest getKey generateKey runProject project spec reporter reporterOptions port env ci record updating ping key logs clearLogs returnPkg version mode headed config exit exitWithCode browser runMode outputPath group groupId parallel parallelId".split(" ") # returns true if the given string has double quote character " # only at the last position. @@ -81,13 +81,12 @@ module.exports = { "exec-path": "execPath" "api-key": "apiKey" "smoke-test": "smokeTest" - "remove-ids": "removeIds" "get-key": "getKey" "new-key": "generateKey" "clear-logs": "clearLogs" "run-project": "runProject" "return-pkg": "returnPkg" - "headless": "isTextTerminal" + "runMode": "isTextTerminal" "exit-with-code": "exitWithCode" "reporter-options": "reporterOptions" "output-path": "outputPath" @@ -157,9 +156,9 @@ module.exports = { options = normalizeBackslashes(options) - ## normalize project to projectPath + ## normalize project to projectRoot if p = options.project or options.runProject - options.projectPath = path.resolve(options.cwd, p) + options.projectRoot = path.resolve(options.cwd, p) ## normalize output path from previous current working directory if op = options.outputPath diff --git a/packages/server/lib/util/ci_provider.coffee b/packages/server/lib/util/ci_provider.coffee index a4cf15364b54..9a742973389e 100644 --- a/packages/server/lib/util/ci_provider.coffee +++ b/packages/server/lib/util/ci_provider.coffee @@ -36,6 +36,7 @@ buildNums = (provider) -> { jenkins: process.env.BUILD_NUMBER travis: process.env.TRAVIS_BUILD_NUMBER semaphore: process.env.SEMAPHORE_BUILD_NUMBER + drone: process.env.DRONE_BUILD_NUMBER }[provider] groupIds = (provider) -> { @@ -69,6 +70,9 @@ params = (provider) -> { semaphore: { repoSlug: process.env.SEMAPHORE_REPO_SLUG } + drone: { + buildUrl: process.env.DRONE_BUILD_LINK + } }[provider] # details = { @@ -86,7 +90,6 @@ params = (provider) -> { # ciUrl: process.env.CI_BUILD_URL # buildNum: process.env.CI_BUILD_NUMBER # } -# "drone": nullDetails # "gitlab": -> { # ciUrl: "#{process.env.CI_PROJECT_URL}/builds/#{process.env.CI_BUILD_ID}" # buildNum: process.env.CI_BUILD_ID @@ -96,7 +99,6 @@ params = (provider) -> { # ciUrl: process.env.BUILD_URL # buildNum: process.env.BUILD_NUMBER # } -# "semaphore": nullDetails # "shippable": nullDetails # "snap": nullDetails # "teamcity": nullDetails diff --git a/packages/server/lib/util/electron_app.coffee b/packages/server/lib/util/electron_app.coffee new file mode 100644 index 000000000000..5656d983a06c --- /dev/null +++ b/packages/server/lib/util/electron_app.coffee @@ -0,0 +1,17 @@ +Promise = require("bluebird") + +ready = -> + app = require("electron").app + + waitForReady = -> + new Promise (resolve, reject) -> + app.on "ready", resolve + + Promise.any([ + waitForReady() + Promise.delay(500) + ]) + +module.exports = { + ready +} diff --git a/packages/server/lib/util/env.coffee b/packages/server/lib/util/env.coffee new file mode 100644 index 000000000000..b1f6b8588c30 --- /dev/null +++ b/packages/server/lib/util/env.coffee @@ -0,0 +1,11 @@ +set = (key, val) -> + process.env[key] = val + +get = (key) -> + process.env[key] + +module.exports = { + set + + get +} diff --git a/packages/server/lib/util/file.coffee b/packages/server/lib/util/file.coffee index a3cbda7e08a2..61757b52e8e4 100644 --- a/packages/server/lib/util/file.coffee +++ b/packages/server/lib/util/file.coffee @@ -1,13 +1,13 @@ _ = require("lodash") -md5 = require("md5") os = require("os") +md5 = require("md5") path = require("path") -Promise = require("bluebird") +debug = require('debug')('cypress:server:file') Queue = require("p-queue") +Promise = require("bluebird") lockFile = Promise.promisifyAll(require("lockfile")) -fs = Promise.promisifyAll(require("fs-extra")) +fs = require("./fs") exit = require("./exit") -log = require('debug')('cypress:server:file') DEBOUNCE_LIMIT = 1000 @@ -80,7 +80,7 @@ module.exports = class Conf _read: -> @_lock() .then => - log('reading JSON file %s', @path) + debug('reading JSON file %s', @path) fs.readJsonAsync(@path, "utf8") .catch (err) => ## default to {} in certain cases, otherwise bubble up error @@ -128,7 +128,7 @@ module.exports = class Conf _write: -> @_lock() .then => - log('writing JSON file %s', @path) + debug('writing JSON file %s', @path) fs.outputJsonAsync(@path, @_cache, {spaces: 2}) .finally => @_unlock() diff --git a/packages/server/lib/fs_warn.coffee b/packages/server/lib/util/fs.coffee similarity index 76% rename from packages/server/lib/fs_warn.coffee rename to packages/server/lib/util/fs.coffee index 90e44eae503c..5a8a62ae5a7c 100644 --- a/packages/server/lib/fs_warn.coffee +++ b/packages/server/lib/util/fs.coffee @@ -1,3 +1,6 @@ +fs = require("fs-extra") +Promise = require("bluebird") + ## warn users if somehow synchronous file methods are invoked ## these methods due to "too many files" errors are a huge pain warnOnSyncFileSystem = -> @@ -5,14 +8,14 @@ warnOnSyncFileSystem = -> console.error "Cypress only works reliably when ALL fs calls are async" console.error "You should modify these sync calls to be async" -topLines = (from, n) -> (text) -> +topLines = (from, n, text) -> text.split("\n").slice(from, n).join("\n") # just hide this function itself # stripping top few lines of the stack getStack = () -> err = new Error() - topLines(3, 10)(err.stack) + topLines(3, 10, err.stack) addSyncFileSystemWarnings = (fs) -> oldExistsSync = fs.existsSync @@ -21,4 +24,8 @@ addSyncFileSystemWarnings = (fs) -> console.error(getStack()) oldExistsSync(filename) -module.exports = addSyncFileSystemWarnings +addSyncFileSystemWarnings(fs) + +promisifiedFs = Promise.promisifyAll(fs) + +module.exports = promisifiedFs diff --git a/packages/server/lib/util/glob.coffee b/packages/server/lib/util/glob.coffee new file mode 100644 index 000000000000..2e079e83300f --- /dev/null +++ b/packages/server/lib/util/glob.coffee @@ -0,0 +1,4 @@ +glob = require("glob") +Promise = require("bluebird") + +module.exports = Promise.promisify(glob) diff --git a/packages/server/lib/util/human_time.coffee b/packages/server/lib/util/human_time.coffee index d7443b551d77..689259f64547 100644 --- a/packages/server/lib/util/human_time.coffee +++ b/packages/server/lib/util/human_time.coffee @@ -1,9 +1,7 @@ moment = require("moment") pluralize = require("pluralize") -module.exports = (ms) -> - msg = [] - +parse = (ms) -> mins = 0 duration = moment.duration(ms) @@ -12,6 +10,17 @@ module.exports = (ms) -> mins = hours * 60 + return { + mins + hours + duration + } + +long = (ms) -> + msg = [] + + { mins, duration } = parse(ms) + if mins += duration.minutes() word = pluralize("minute", mins) msg.push(mins + " " + word) @@ -22,4 +31,33 @@ module.exports = (ms) -> msg.push(secs + " " + word) - msg.join(", ") \ No newline at end of file + msg.join(", ") + +short = (ms) -> + msg = [] + + { mins, duration } = parse(ms) + + if mins += duration.minutes() + msg.push(mins + "m") + + secs = duration.seconds() + + if secs + msg.push(secs + "s") + else + if not mins + millis = duration.milliseconds() + + if millis + msg.push(millis + "ms") + else + msg.push(secs + "s") + + msg.join(", ") + +module.exports = { + long + + short +} diff --git a/packages/server/lib/util/path_helpers.coffee b/packages/server/lib/util/path_helpers.coffee index 833ccf7b275f..06518b1ddb16 100644 --- a/packages/server/lib/util/path_helpers.coffee +++ b/packages/server/lib/util/path_helpers.coffee @@ -1,8 +1,6 @@ -fs = require("fs") path = require("path") Promise = require("bluebird") - -fs = Promise.promisifyAll(fs) +fs = require("./fs") isIntegrationTestRe = /^integration/ isUnitTestRe = /^unit/ diff --git a/packages/server/lib/util/random.coffee b/packages/server/lib/util/random.coffee new file mode 100644 index 000000000000..7a7e9e972d41 --- /dev/null +++ b/packages/server/lib/util/random.coffee @@ -0,0 +1,12 @@ +random = require("randomstring") + +id = -> + ## return a random id + random.generate({ + length: 5 + capitalization: "lowercase" + }) + +module.exports = { + id +} diff --git a/packages/server/lib/util/routes.coffee b/packages/server/lib/util/routes.coffee index 83484ca60015..60c7e4dd9789 100644 --- a/packages/server/lib/util/routes.coffee +++ b/packages/server/lib/util/routes.coffee @@ -10,15 +10,15 @@ routes = { ping: "ping" signin: "signin" signout: "signout" - runs: "builds" - instances: "builds/:id/instances" + runs: "runs" + instances: "runs/:id/instances" instance: "instances/:id" instanceStdout:"instances/:id/stdout" orgs: "organizations" projects: "projects" project: "projects/:id" projectToken: "projects/:id/token" - projectRuns: "projects/:id/builds" + projectRuns: "projects/:id/runs" projectRecordKeys: "projects/:id/keys" exceptions: "exceptions" membershipRequests: "projects/:id/membership_requests" @@ -35,7 +35,7 @@ parseArgs = (url, args = []) -> return url -Routes = _.reduce routes, (memo, value, key) -> +routes = _.reduce routes, (memo, value, key) -> memo[key] = (args...) -> url = new UrlParse(api_url, true) url.set("pathname", value) if value @@ -44,4 +44,4 @@ Routes = _.reduce routes, (memo, value, key) -> memo , {} -module.exports = Routes +module.exports = routes diff --git a/packages/server/lib/util/saved_state.coffee b/packages/server/lib/util/saved_state.coffee index 1a852bc29a5b..a5c90594d5d0 100644 --- a/packages/server/lib/util/saved_state.coffee +++ b/packages/server/lib/util/saved_state.coffee @@ -1,26 +1,26 @@ -log = require('../log') -cwd = require('../cwd') -fs = require('fs-extra') -md5 = require('md5') -sanitize = require("sanitize-filename") +md5 = require("md5") +path = require("path") Promise = require("bluebird") -{ basename, join, isAbsolute } = require('path') +sanitize = require("sanitize-filename") +log = require("../log") +cwd = require("../cwd") +fs = require("../util/fs") -toHashName = (projectPath) -> - throw new Error("Missing project path") unless projectPath - throw new Error("Expected project absolute path, not just a name #{projectPath}") unless isAbsolute(projectPath) - name = sanitize(basename(projectPath)) - hash = md5(projectPath) +toHashName = (projectRoot) -> + throw new Error("Missing project path") unless projectRoot + throw new Error("Expected project absolute path, not just a name #{projectRoot}") unless path.isAbsolute(projectRoot) + name = sanitize(path.basename(projectRoot)) + hash = md5(projectRoot) "#{name}-#{hash}" # async promise-returning method -formStatePath = (projectPath) -> +formStatePath = (projectRoot) -> Promise.resolve() .then -> log('making saved state from %s', cwd()) - if projectPath - log('for project path %s', projectPath) - return projectPath + if projectRoot + log('for project path %s', projectRoot) + return projectRoot else log('missing project path, looking for project here') @@ -29,17 +29,17 @@ formStatePath = (projectPath) -> .then (found) -> if found log('found cypress file %s', cypressJsonPath) - projectPath = cwd() - return projectPath + projectRoot = cwd() + return projectRoot - .then (projectPath) -> + .then (projectRoot) -> fileName = "state.json" - if projectPath - log("state path for project #{projectPath}") - statePath = join(toHashName(projectPath), fileName) + if projectRoot + log("state path for project #{projectRoot}") + statePath = path.join(toHashName(projectRoot), fileName) else log("state path for global mode") - statePath = join("__global__", fileName) + statePath = path.join("__global__", fileName) return statePath diff --git a/packages/server/lib/util/security.coffee b/packages/server/lib/util/security.coffee index af90c99a18f4..5729d6266702 100644 --- a/packages/server/lib/util/security.coffee +++ b/packages/server/lib/util/security.coffee @@ -5,18 +5,21 @@ replacestream = require("replacestream") topOrParentEqualityBeforeRe = /((?:window|self)(?:\.|\[['"](?:top|self)['"]\])?\s*[!=][=]\s*(?:(?:window|self)(?:\.|\[['"]))?)(top|parent)/g topOrParentEqualityAfterRe = /(top|parent)((?:["']\])?\s*[!=][=]=?\s*(?:window|self))/g topOrParentLocationOrFramesRe = /([^\da-zA-Z])(top|parent)([.])(location|frames)/g +jiraTopWindowGetterRe = /(!function\s*\((\w{1})\)\s*{\s*return\s*\w{1}\s*(?:={2,})\s*\w{1}\.parent)(\s*}\(\w{1}\))/g strip = (html) -> html .replace(topOrParentEqualityBeforeRe, "$1self") .replace(topOrParentEqualityAfterRe, "self$2") .replace(topOrParentLocationOrFramesRe, "$1self$3$4") + .replace(jiraTopWindowGetterRe, "$1 || $2.parent.__Cypress__$3") stripStream = -> pumpify( replacestream(topOrParentEqualityBeforeRe, "$1self") replacestream(topOrParentEqualityAfterRe, "self$2") replacestream(topOrParentLocationOrFramesRe, "$1self$3$4") + replacestream(jiraTopWindowGetterRe, "$1 || $2.parent.__Cypress__$3") ) module.exports = { diff --git a/packages/server/lib/util/settings.coffee b/packages/server/lib/util/settings.coffee index 57b273a969a7..748dee9122c3 100644 --- a/packages/server/lib/util/settings.coffee +++ b/packages/server/lib/util/settings.coffee @@ -1,9 +1,9 @@ _ = require("lodash") Promise = require("bluebird") path = require("path") -fs = require("fs-extra") errors = require("../errors") log = require("../log") +fs = require("../util/fs") ## TODO: ## think about adding another PSemaphore @@ -11,8 +11,6 @@ log = require("../log") ## settings at the same time something else ## is potentially reading it -fs = Promise.promisifyAll(fs) - flattenCypress = (obj) -> if cypress = obj.cypress return cypress @@ -139,3 +137,6 @@ module.exports = pathToCypressJson: (projectRoot) -> @_pathToFile(projectRoot, "cypress.json") + + pathToCypressEnvJson: (projectRoot) -> + @_pathToFile(projectRoot, "cypress.env.json") diff --git a/packages/server/lib/util/specs.coffee b/packages/server/lib/util/specs.coffee new file mode 100644 index 000000000000..06f65d6fd3b4 --- /dev/null +++ b/packages/server/lib/util/specs.coffee @@ -0,0 +1,123 @@ +_ = require("lodash") +la = require("lazy-ass") +path = require("path") +check = require("check-more-types") +debug = require("debug")("cypress:server:specs") +minimatch = require("minimatch") +glob = require("./glob") + +MINIMATCH_OPTIONS = { dot: true, matchBase: true } + +getPatternRelativeToProjectRoot = (specPattern, projectRoot) -> + _.map specPattern, (p) -> + path.relative(projectRoot, p) + +find = (config, specPattern) -> + la(check.maybe.strings(specPattern), "invalid spec pattern", specPattern) + + integrationFolderPath = config.integrationFolder + + debug( + "looking for test specs in the folder:", + integrationFolderPath + ) + + ## support files are not automatically + ## ignored because only _fixtures are hard + ## coded. the rest is simply whatever is in + ## the javascripts array + + if config.fixturesFolder + fixturesFolderPath = path.join( + config.fixturesFolder, + "**", + "*" + ) + + supportFilePath = config.supportFile or [] + + ## map all of the javascripts to the project root + ## TODO: think about moving this into config + ## and mapping each of the javascripts into an + ## absolute path + javascriptsPaths = _.map config.javascripts, (js) -> + path.join(config.projectRoot, js) + + ## ignore fixtures + javascripts + options = { + sort: true + absolute: true + cwd: integrationFolderPath + ignore: _.compact(_.flatten([ + javascriptsPaths + supportFilePath + fixturesFolderPath + ])) + } + + ## filePath = /Users/bmann/Dev/my-project/cypress/integration/foo.coffee + ## integrationFolderPath = /Users/bmann/Dev/my-project/cypress/integration + ## relativePathFromIntegrationFolder = foo.coffee + ## relativePathFromProjectRoot = cypress/integration/foo.coffee + + relativePathFromIntegrationFolder = (file) -> + path.relative(integrationFolderPath, file) + + relativePathFromProjectRoot = (file) -> + path.relative(config.projectRoot, file) + + setNameParts = (file) -> + debug("found spec file %s", file) + + if not path.isAbsolute(file) + throw new Error("Cannot set parts of file from non-absolute path #{file}") + + { + name: relativePathFromIntegrationFolder(file) + path: relativePathFromProjectRoot(file) + absolute: file + } + + ignorePatterns = [].concat(config.ignoreTestFiles) + + ## a function which returns true if the file does NOT match + ## all of our ignored patterns + doesNotMatchAllIgnoredPatterns = (file) -> + ## using {dot: true} here so that folders with a '.' in them are matched + ## as regular characters without needing an '.' in the + ## using {matchBase: true} here so that patterns without a globstar ** + ## match against the basename of the file + _.every ignorePatterns, (pattern) -> + not minimatch(file, pattern, MINIMATCH_OPTIONS) + + matchesSpecPattern = (file) -> + if not specPattern + return true + + matchesPattern = (pattern) -> + minimatch(file, pattern, MINIMATCH_OPTIONS) + + ## check to see if the file matches + ## any of the spec patterns array + return _ + .chain([]) + .concat(specPattern) + .some(matchesPattern) + .value() + + ## grab all the files + glob(config.testFiles, options) + + ## filter out anything that matches our + ## ignored test files glob + .filter(doesNotMatchAllIgnoredPatterns) + .filter(matchesSpecPattern) + .map(setNameParts) + .tap (files) -> + debug("found %d spec files: %o", files.length, files) + +module.exports = { + find + + getPatternRelativeToProjectRoot +} diff --git a/packages/server/lib/util/terminal-size.coffee b/packages/server/lib/util/terminal-size.coffee new file mode 100644 index 000000000000..2d91f06ac4cf --- /dev/null +++ b/packages/server/lib/util/terminal-size.coffee @@ -0,0 +1,8 @@ +termSize = require("term-size") + +get = -> + termSize() + +module.exports = { + get +} diff --git a/packages/server/lib/util/terminal.coffee b/packages/server/lib/util/terminal.coffee index a75c657711ad..6d94093d50c2 100644 --- a/packages/server/lib/util/terminal.coffee +++ b/packages/server/lib/util/terminal.coffee @@ -1,48 +1,195 @@ _ = require("lodash") chalk = require("chalk") +Table = require("cli-table2") +utils = require("cli-table2/src/utils") +widestLine = require("widest-line") +terminalSize = require("./terminal-size") -module.exports = { - header: (message, options = {}) -> - _.defaults(options, { - color: null - }) +MAXIMUM_SIZE = 100 +EXPECTED_SUM = 100 + +getMaximumColumns = -> + ## get the maximum amount of columns + ## that can fit in the terminal + Math.min(MAXIMUM_SIZE, terminalSize.get().columns) + +getBordersLength = (left, right) -> + _ + .chain([left, right]) + .compact() + .map(widestLine) + .sum() + .value() + +convertDecimalsToNumber = (colWidths, cols) -> + sum = _.sum(colWidths) + + if sum isnt EXPECTED_SUM + throw new Error("Expected colWidths array to sum to: #{EXPECTED_SUM}, instead got: #{sum}") + + [50, 10, 25] + + widths = _.map colWidths, (width) -> + ## easier to deal with numbers than floats... + num = (cols * width) / EXPECTED_SUM + + Math.floor(num) + + total = _.sum(widths) + + ## if we got a sum less than the total + ## columns, then add the difference to + ## the first element in the array of widths + if (diff = cols - total) > 0 + first = widths[0] + widths[0] += diff + + widths + +renderTables = (tables...) -> + _ + .chain([]) + .concat(tables) + .invokeMap("toString") + .join("\n") + .value() + +getChars = (type) -> + switch type + when "border" + return { + "top-mid": "" + "top-left": " ┌" + "left": " │" + "left-mid": " ├" + "middle": "" + "mid-mid": "" + "bottom-mid": "" + "bottom-left": " └" + } + when "noBorder" + return { + "top": "" + "top-mid": "" + "top-left": "" + "top-right": "" + "left": " " + "left-mid": "" + "middle": "" + "mid": "" + "mid-mid": "" + "right": "" + "right-mid": "" + "bottom": "" + "bottom-left": "" + "bottom-mid": "" + "bottom-right": "" + } + when "outsideBorder" + return { + # "top": "" + "top-left": " ┌" + "top-mid": "" + "left": " │" + "left-mid": "" + "middle": "" + "mid": "" + "mid-mid": "" + "right-mid": "" + "bottom-mid": "" + "bottom-left": " └" + } + when "pageDivider" + return { + "top": "─" + "top-mid": "" + "top-left": "" + "top-right": "" + "bottom": "" + "bottom-mid": "" + "bottom-left": "" + "bottom-right": "" + "left": "" + "left-mid": "" + "mid": "" + "mid-mid": "" + "right": "" + "right-mid": "" + "middle": "" + } - message = " (" + chalk.underline.bold(message) + ")" +wrapBordersInGray = (chars) -> + _.mapValues chars, (char) -> + if char + chalk.gray(char) + else + char - if c = options.color - colors = [].concat(c) +table = (options = {}) -> + { colWidths, type } = options - message = _.reduce colors, (memo, color) -> - chalk[color](memo) - , message + defaults = utils.mergeOptions({}) - console.log(message) + chars = _.defaults(getChars(type), defaults.chars) - divider: (message, options = {}) -> - _.defaults(options, { - width: 100 - preBreak: false - postBreak: false - }) + _.defaultsDeep(options, { + chars + style: { + head: [] + border: [] + 'padding-left': 1 + 'padding-right': 1 + } + }) - w = options.width / 2 + { chars } = options - a = -> - Array(Math.floor(w)) + borders = getBordersLength(chars.left, chars.right) - message = " " + message + " " + options.chars = wrapBordersInGray(chars) + + if colWidths + ## subtract borders to get the actual size + ## so it displaces a maximum number of columns + cols = getMaximumColumns() - borders + options.colWidths = convertDecimalsToNumber(colWidths, cols) + + new Table(options) + +header = (message, options = {}) -> + _.defaults(options, { + color: null + }) + + message = " (" + chalk.underline.bold(message) + ")" + + if c = options.color + colors = [].concat(c) + + message = _.reduce colors, (memo, color) -> + chalk[color](memo) + , message + + console.log(message) + +divider = (symbol, color = "gray") -> + cols = getMaximumColumns() + + str = symbol.repeat(cols) + + console.log(chalk[color](str)) + +module.exports = { + table - message = a().concat(message, a()).join("=") + header - if c = options.color - message = chalk[c](message) + divider - if options.preBreak - console.log("") + renderTables - console.log(message) + getMaximumColumns - if options.postBreak - console.log("") + convertDecimalsToNumber -} \ No newline at end of file +} diff --git a/packages/server/lib/util/tty.js b/packages/server/lib/util/tty.js index 38e137a9ecf0..a7e68ec9d6af 100644 --- a/packages/server/lib/util/tty.js +++ b/packages/server/lib/util/tty.js @@ -1,23 +1,37 @@ const tty = require('tty') -const override = () => { - // if we're being told to force STDERR - if (process.env.FORCE_STDERR_TTY === '1') { - const isatty = tty.isatty - const _fd = process.stderr.fd +function patchStream (patched, name) { + const stream = process[name] - process.stderr.isTTY = true + stream.isTTY = true - tty.isatty = function (fd) { - if (fd === _fd) { - // force stderr to return true - return true - } + patched[stream.fd] = true +} - // else pass through - return isatty.call(this, fd) +const override = () => { + const isatty = tty.isatty + + const patched = { + 0: false, + 1: false, + 2: false, + } + + tty.isatty = function (fd) { + if (patched[fd]) { + // force stderr to return true + return true } + + // else pass through + return isatty.call(tty, fd) } + + if (process.env.FORCE_STDIN_TTY === '1') patchStream(patched, 'stdin') + if (process.env.FORCE_STDOUT_TTY === '1') patchStream(patched, 'stdout') + if (process.env.FORCE_STDERR_TTY === '1') patchStream(patched, 'stderr') + + return } module.exports = { diff --git a/packages/server/lib/video.coffee b/packages/server/lib/video.coffee index 6ba13e850bae..76a85717403a 100644 --- a/packages/server/lib/video.coffee +++ b/packages/server/lib/video.coffee @@ -1,13 +1,11 @@ _ = require("lodash") -fs = require("fs-extra") utils = require("fluent-ffmpeg/lib/utils") +debug = require("debug")("cypress:server:video") ffmpeg = require("fluent-ffmpeg") stream = require("stream") Promise = require("bluebird") ffmpegPath = require("@ffmpeg-installer/ffmpeg").path -debug = require("debug")("cypress:server:video") - -fs = Promise.promisifyAll(fs) +fs = require("./util/fs") ffmpeg.setFfmpegPath(ffmpegPath) @@ -97,7 +95,7 @@ module.exports = { .on "end", -> debug("ffmpeg ended") - + ended.resolve() .save(name) diff --git a/packages/server/lib/watchers.coffee b/packages/server/lib/watchers.coffee index b9126d658170..a741e841d7e4 100644 --- a/packages/server/lib/watchers.coffee +++ b/packages/server/lib/watchers.coffee @@ -1,5 +1,6 @@ -_ = require("lodash") -chokidar = require("chokidar") +_ = require("lodash") +chokidar = require("chokidar") +dependencyTree = require("dependency-tree") pathHelpers = require("./util/path_helpers") class Watchers @@ -38,6 +39,17 @@ class Watchers return @ + watchTree: (filePath, options = {}) -> + files = dependencyTree.toList({ + filename: filePath + directory: process.cwd() + filter: (filePath) -> + filePath.indexOf("node_modules") is -1 + }) + + _.each files, (file) => + @watch(file, options) + _add: (filePath, watcher) -> @_remove(filePath) diff --git a/packages/server/package.json b/packages/server/package.json index f2128cc67e71..95295d49b181 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -30,10 +30,13 @@ "lib" ], "devDependencies": { + "@cypress/json-schemas": "5.0.1", "@cypress/sinon-chai": "^1.0.0", "bin-up": "^1.1.0", "body-parser": "1.12.4", + "chai-uuid": "^1.0.6", "chokidar-cli": "^1.2.0", + "cli-table2": "^0.2.0", "codecov": "^1.0.1", "coffee-coverage": "^1.0.1", "cors": "^2.8.3", @@ -43,6 +46,7 @@ "express-session": "^1.14.1", "express-useragent": "^1.0.4", "https-proxy-agent": "^1.0.0", + "image-size": "^0.5.0", "inquirer": "3.0.6", "istanbul": "^0.4.2", "mockery": "^1.4.0", @@ -62,7 +66,7 @@ "supertest-session": "0.0.7", "through2": "0.6.3", "vagrant": "0.0.1", - "ws": "^1.0.1", + "ws": "^5.1.1", "xvfb": "cypress-io/node-xvfb#22e3783c31d81ebe64d8c0df491ea00cdc74726a", "xvfb-maybe": "cypress-io/xvfb-maybe#c4a810c42d603949cd63b8cf245f6c239331d370" }, @@ -81,7 +85,7 @@ "browserify": "^13.1.1", "bytes": "^2.4.0", "chai": "^1.9.2", - "chalk": "^2.0.1", + "chalk": "^2.4.1", "check-more-types": "^2.24.0", "chokidar": "1.6.0", "cjsxify": "^0.3.0", @@ -93,6 +97,7 @@ "cookie-parser": "^1.3.3", "data-uri-to-buffer": "0.0.4", "debug": "^2.6.8", + "dependency-tree": "^6.0.1", "electron-context-menu": "^0.8.0", "electron-positioner": "3.0.0", "errorhandler": "1.1.1", @@ -111,11 +116,14 @@ "http-status-codes": "^1.0.6", "human-interval": "^0.1.5", "image-size": "^0.5.0", + "is-fork-pr": "2.0.0", + "jimp": "^0.2.28", "jsonlint": "^1.6.2", "konfig": "^0.2.0", "lazy-ass": "^1.6.0", "lockfile": "^1.0.3", "lodash": "4.17.4", + "log-symbols": "^2.2.0", "md5": "^2.2.1", "method-override": "^2.3.1", "mime": "1.2.11", @@ -148,13 +156,13 @@ "server-destroy": "1.0.1", "shell-env": "^0.3.0", "signal-exit": "^3.0.2", - "sinon": "1.17.7", - "sinon-as-promised": "3.0.1", + "sinon": "^5.0.0", "string-to-stream": "^1.0.1", "strip-ansi": "^3.0.1", "supports-color": "^5.1.0", "syntax-error": "^1.1.4", "tar-fs": "^1.11.1", + "term-size": "^1.2.0", "through": "2.3.6", "tough-cookie": "2.3.0", "trash": "4.0.0", @@ -162,6 +170,7 @@ "underscore.string": "3.3.4", "url-parse": "^1.1.7", "watchify": "^3.9.0", + "widest-line": "^2.0.0", "winston": "^0.9.0" } } diff --git a/packages/server/test/e2e/issue_149_spec.coffee b/packages/server/test/e2e/issue_149_spec.coffee index f0b77c826a0c..4c69d48c47bd 100644 --- a/packages/server/test/e2e/issue_149_spec.coffee +++ b/packages/server/test/e2e/issue_149_spec.coffee @@ -1,4 +1,4 @@ -fs = require("fs-extra") +fs = require("../../lib/util/fs") Fixtures = require("../support/helpers/fixtures") e2e = require("../support/helpers/e2e") diff --git a/packages/server/test/e2e/issue_674_spec.coffee b/packages/server/test/e2e/issue_674_spec.coffee index a7a577457f20..6b15a231e606 100644 --- a/packages/server/test/e2e/issue_674_spec.coffee +++ b/packages/server/test/e2e/issue_674_spec.coffee @@ -9,5 +9,5 @@ describe "e2e issue 674", -> e2e.exec(@, { spec: "issue_674_spec.coffee" snapshot: true - expectedExitCode: 2 ## TODO: this is changing to become '1' in Cypress 3.0 + expectedExitCode: 1 }) diff --git a/packages/server/test/e2e/new_project_spec.coffee b/packages/server/test/e2e/new_project_spec.coffee index 072384f5fc18..433fdb51afaa 100644 --- a/packages/server/test/e2e/new_project_spec.coffee +++ b/packages/server/test/e2e/new_project_spec.coffee @@ -1,6 +1,6 @@ -fs = require("fs-extra") path = require("path") Promise = require("bluebird") +fs = require("../../lib/util/fs") Fixtures = require("../support/helpers/fixtures") e2e = require("../support/helpers/e2e") diff --git a/packages/server/test/e2e/record_spec.coffee b/packages/server/test/e2e/record_spec.coffee new file mode 100644 index 000000000000..718d88fd4896 --- /dev/null +++ b/packages/server/test/e2e/record_spec.coffee @@ -0,0 +1,641 @@ +_ = require("lodash") +Promise = require("bluebird") +bodyParser = require("body-parser") +jsonSchemas = require("@cypress/json-schemas").api +e2e = require("../support/helpers/e2e") + +postRunResponse = jsonSchemas.getExample("postRunResponse")("2.0.0") +postRunInstanceResponse = jsonSchemas.getExample("postRunInstanceResponse")("2.0.0") + +{ runId, planId, machineId, runUrl } = postRunResponse +{ instanceId } = postRunInstanceResponse + +requests = null + +getRequestUrls = -> + _.map(requests, "url") + +getSchemaErr = (err, schema) -> + { + errors: err.errors + object: err.object + example: err.example + message: "Request should follow #{schema} schema" + } + +getResponse = (responseSchema) -> + if _.isObject(responseSchema) + return responseSchema + + [ name, version ] = responseSchema.split("@") + + jsonSchemas.getExample(name)(version) + +sendResponse = (req, res, responseSchema) -> + if _.isFunction(responseSchema) + return responseSchema(req, res) + + res.json(getResponse(responseSchema)) + +ensureSchema = (requestSchema, responseSchema) -> + if requestSchema + [ name, version ] = requestSchema.split("@") + + return (req, res) -> + { body } = req + + try + if requestSchema + jsonSchemas.assertSchema(name, version)(body) + + sendResponse(req, res, responseSchema) + + key = [req.method, req.url].join(" ") + + requests.push({ + url: key + body + }) + catch err + res.status(412).json(getSchemaErr(err, requestSchema)) + +sendUploadUrls = (req, res) -> + { body } = req + + num = 0 + + json = {} + + if body.video + json.videoUploadUrl = "http://localhost:1234/videos/video.mp4" + + screenshotUploadUrls = _.map body.screenshots, (s) -> + num += 1 + + return { + screenshotId: s.screenshotId + uploadUrl: "http://localhost:1234/screenshots/#{num}.png" + } + + if screenshotUploadUrls.length + json.screenshotUploadUrls = screenshotUploadUrls + + res.json(json) + +onServer = (routes) -> + return (app) -> + app.use(bodyParser.json()) + + _.each routes, (route) -> + app[route.method](route.url, ensureSchema( + route.req, + route.res + )) + +setup = (routes, settings = {}) -> + e2e.setup({ + settings: _.extend({ + projectId: "pid123" + videoUploadOnPasses: false + }, settings) + servers: { + port: 1234 + onServer: onServer(routes) + } + }) + +defaultRoutes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: postRunResponse + }, { + method: "post" + url: "/runs/:id/instances" + req: "postRunInstanceRequest@2.0.0", + res: postRunInstanceResponse + }, { + method: "put" + url: "/instances/:id" + req: "putInstanceRequest@2.0.0", + res: sendUploadUrls + }, { + method: "put" + url: "/instances/:id/stdout" + req: "putInstanceStdoutRequest@1.0.0", + res: (req, res) -> res.sendStatus(200) + }, { + method: "put" + url: "/videos/:name" + res: (req, res) -> + Promise.delay(500) + .then -> + res.sendStatus(200) + }, { + method: "put" + url: "/screenshots/:name" + res: (req, res) -> res.sendStatus(200) + } +] + +describe "e2e record", -> + env = _.clone(process.env) + + beforeEach -> + process.env = env + + requests = [] + + context "passing", -> + setup(defaultRoutes) + + it "passes", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record*" + record: true + snapshot: true + expectedExitCode: 3 + }) + .get("stdout") + .then (stdout) -> + expect(stdout).to.include("Run URL:") + expect(stdout).to.include(runUrl) + + urls = getRequestUrls() + + ## first create run request + expect(urls[0]).to.eq("POST /runs") + + ## grab the first set of 4 + firstInstanceSet = urls.slice(1, 5) + + expect(firstInstanceSet).to.deep.eq([ + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + "PUT /videos/video.mp4" + "PUT /instances/#{instanceId}/stdout" + ]) + + ## grab the second set of 5 + secondInstanceSet = urls.slice(5, 10) + + expect(secondInstanceSet).to.have.members([ + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + "PUT /videos/video.mp4" + "PUT /screenshots/1.png" + "PUT /instances/#{instanceId}/stdout" + ]) + + ## grab the third set of 5 + thirdInstanceSet = urls.slice(10, 14) + + ## no video because no tests failed + expect(thirdInstanceSet).to.deep.eq([ + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + "PUT /screenshots/1.png" + "PUT /instances/#{instanceId}/stdout" + ]) + + ## grab the forth set of 5 + forthInstanceSet = urls.slice(14, 19) + + expect(forthInstanceSet).to.have.members([ + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + "PUT /videos/video.mp4" + "PUT /screenshots/1.png" + "PUT /instances/#{instanceId}/stdout" + ]) + + postRun = requests[0] + + ## ensure its relative to projectRoot + expect(postRun.body.specs).to.deep.eq([ + "cypress/integration/record_error_spec.coffee" + "cypress/integration/record_fail_spec.coffee" + "cypress/integration/record_pass_spec.coffee" + "cypress/integration/record_uncaught_spec.coffee" + ]) + expect(postRun.body.projectId).to.eq("pid123") + expect(postRun.body.recordKey).to.eq("f858a2bc-b469-4e48-be67-0876339ee7e1") + expect(postRun.body.specPattern).to.eq("cypress/integration/record*") + + firstInstance = requests[1] + expect(firstInstance.body.planId).to.eq(planId) + expect(firstInstance.body.machineId).to.eq(machineId) + expect(firstInstance.body.spec).to.eq( + "cypress/integration/record_error_spec.coffee" + ) + + firstInstancePut = requests[2] + expect(firstInstancePut.body.error).to.include("Oops...we found an error preparing this test file") + expect(firstInstancePut.body.tests).to.be.null + expect(firstInstancePut.body.hooks).to.be.null + expect(firstInstancePut.body.screenshots).to.have.length(0) + expect(firstInstancePut.body.stats.tests).to.eq(0) + expect(firstInstancePut.body.stats.failures).to.eq(1) + expect(firstInstancePut.body.stats.passes).to.eq(0) + + firstInstanceStdout = requests[4] + expect(firstInstanceStdout.body.stdout).to.include("record_error_spec.coffee") + + secondInstance = requests[5] + expect(secondInstance.body.planId).to.eq(planId) + expect(secondInstance.body.machineId).to.eq(machineId) + expect(secondInstance.body.spec).to.eq( + "cypress/integration/record_fail_spec.coffee" + ) + + secondInstancePut = requests[6] + expect(secondInstancePut.body.error).to.be.null + expect(secondInstancePut.body.tests).to.have.length(2) + expect(secondInstancePut.body.hooks).to.have.length(1) + expect(secondInstancePut.body.screenshots).to.have.length(1) + expect(secondInstancePut.body.stats.tests).to.eq(2) + expect(secondInstancePut.body.stats.failures).to.eq(1) + expect(secondInstancePut.body.stats.passes).to.eq(0) + expect(secondInstancePut.body.stats.skipped).to.eq(1) + + secondInstanceStdout = requests[9] + expect(secondInstanceStdout.body.stdout).to.include("record_fail_spec.coffee") + expect(secondInstanceStdout.body.stdout).not.to.include("record_error_spec.coffee") + + thirdInstance = requests[10] + expect(thirdInstance.body.planId).to.eq(planId) + expect(thirdInstance.body.machineId).to.eq(machineId) + expect(thirdInstance.body.spec).to.eq( + "cypress/integration/record_pass_spec.coffee" + ) + + thirdInstancePut = requests[11] + expect(thirdInstancePut.body.error).to.be.null + expect(thirdInstancePut.body.tests).to.have.length(2) + expect(thirdInstancePut.body.hooks).to.have.length(0) + expect(thirdInstancePut.body.screenshots).to.have.length(1) + expect(thirdInstancePut.body.stats.tests).to.eq(2) + expect(thirdInstancePut.body.stats.passes).to.eq(1) + expect(thirdInstancePut.body.stats.failures).to.eq(0) + expect(thirdInstancePut.body.stats.pending).to.eq(1) + + thirdInstanceStdout = requests[13] + expect(thirdInstanceStdout.body.stdout).to.include("record_pass_spec.coffee") + expect(thirdInstanceStdout.body.stdout).not.to.include("record_error_spec.coffee") + expect(thirdInstanceStdout.body.stdout).not.to.include("record_fail_spec.coffee") + + fourthInstance = requests[14] + expect(fourthInstance.body.planId).to.eq(planId) + expect(fourthInstance.body.machineId).to.eq(machineId) + expect(fourthInstance.body.spec).to.eq( + "cypress/integration/record_uncaught_spec.coffee" + ) + + fourthInstancePut = requests[15] + expect(fourthInstancePut.body.error).to.be.null + expect(fourthInstancePut.body.tests).to.have.length(1) + expect(fourthInstancePut.body.hooks).to.have.length(0) + expect(fourthInstancePut.body.screenshots).to.have.length(1) + expect(fourthInstancePut.body.stats.tests).to.eq(1) + expect(fourthInstancePut.body.stats.failures).to.eq(1) + expect(fourthInstancePut.body.stats.passes).to.eq(0) + + forthInstanceStdout = requests[18] + expect(forthInstanceStdout.body.stdout).to.include("record_uncaught_spec.coffee") + expect(forthInstanceStdout.body.stdout).not.to.include("record_error_spec.coffee") + expect(forthInstanceStdout.body.stdout).not.to.include("record_fail_spec.coffee") + expect(forthInstanceStdout.body.stdout).not.to.include("record_pass_spec.coffee") + + context "misconfiguration", -> + setup([]) + + it "errors and exits when no specs found", -> + e2e.exec(@, { + spec: "notfound/**" + snapshot: true + expectedExitCode: 1 + }) + .then -> + expect(getRequestUrls()).to.be.empty + + it "errors and exits when no browser found", -> + e2e.exec(@, { + browser: "browserDoesNotExist" + spec: "record_pass*" + snapshot: true + expectedExitCode: 1 + }) + .then -> + expect(getRequestUrls()).to.be.empty + + context "projectId", -> + e2e.setup() + + it "errors and exits without projectId", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 1 + }) + + context "recordKey", -> + setup(defaultRoutes) + + it "errors and exits without recordKey", -> + e2e.exec(@, { + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 1 + }) + .then -> + expect(getRequestUrls()).to.be.empty + + it "warns but does not exit when is forked pr", -> + process.env.CIRCLE_PR_NUMBER = "123" + process.env.CIRCLE_PR_USERNAME = "brian-mann" + process.env.CIRCLE_PR_REPONAME = "cypress" + + e2e.exec(@, { + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + .then -> + expect(getRequestUrls()).to.be.empty + + context "video recording", -> + setup(defaultRoutes, { + videoRecording: false + }) + + it "does not upload when not enabled", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + + context "api interaction errors", -> + describe "recordKey and projectId", -> + routes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: (req, res) -> res.sendStatus(401) + } + ] + + setup(routes) + + it "errors and exits on 401", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 1 + }) + + describe "project 404", -> + routes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: (req, res) -> res.sendStatus(404) + } + ] + + setup(routes) + + it "errors and exits", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 1 + }) + + describe "create run", -> + routes = [{ + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: (req, res) -> res.sendStatus(500) + }] + + setup(routes) + + it "warns and does not create or update instances", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + .then -> + urls = getRequestUrls() + + expect(urls).to.deep.eq([ + "POST /runs" + ]) + + describe "create instance", -> + routes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: postRunResponse + }, { + method: "post" + url: "/runs/:id/instances" + req: "postRunInstanceRequest@2.0.0", + res: (req, res) -> res.sendStatus(500) + } + ] + + setup(routes) + + it "does not update instance", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + .then -> + urls = getRequestUrls() + + expect(urls).to.deep.eq([ + "POST /runs" + "POST /runs/#{runId}/instances" + ]) + + describe "update instance", -> + routes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: postRunResponse + }, { + method: "post" + url: "/runs/:id/instances" + req: "postRunInstanceRequest@2.0.0", + res: postRunInstanceResponse + }, { + method: "put" + url: "/instances/:id" + req: "putInstanceRequest@2.0.0", + res: (req, res) -> res.sendStatus(500) + } + ] + + setup(routes) + + it "does not update instance stdout", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + .then -> + urls = getRequestUrls() + + expect(urls).to.deep.eq([ + "POST /runs" + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + ]) + + describe "update instance stdout", -> + routes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: postRunResponse + }, { + method: "post" + url: "/runs/:id/instances" + req: "postRunInstanceRequest@2.0.0", + res: postRunInstanceResponse + }, { + method: "put" + url: "/instances/:id" + req: "putInstanceRequest@2.0.0", + res: sendUploadUrls + }, { + method: "put" + url: "/instances/:id/stdout" + req: "putInstanceStdoutRequest@1.0.0", + res: (req, res) -> res.sendStatus(500) + }, { + method: "put" + url: "/videos/:name" + res: (req, res) -> + Promise.delay(500) + .then -> + res.sendStatus(200) + }, { + method: "put" + url: "/screenshots/:name" + res: (req, res) -> res.sendStatus(200) + } + ] + + setup(routes) + + it "warns but proceeds", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + .then -> + urls = getRequestUrls() + + expect(urls).to.deep.eq([ + "POST /runs" + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + "PUT /screenshots/1.png" + "PUT /instances/#{instanceId}/stdout" + ]) + + describe "uploading assets", -> + routes = [ + { + method: "post" + url: "/runs" + req: "postRunRequest@2.0.0", + res: postRunResponse + }, { + method: "post" + url: "/runs/:id/instances" + req: "postRunInstanceRequest@2.0.0", + res: postRunInstanceResponse + }, { + method: "put" + url: "/instances/:id" + req: "putInstanceRequest@2.0.0", + res: sendUploadUrls + }, { + method: "put" + url: "/instances/:id/stdout" + req: "putInstanceStdoutRequest@1.0.0", + res: (req, res) -> res.sendStatus(200) + }, { + method: "put" + url: "/videos/:name" + res: (req, res) -> + Promise.delay(500) + .then -> + res.sendStatus(500) + }, { + method: "put" + url: "/screenshots/:name" + res: (req, res) -> res.sendStatus(500) + } + ] + + setup(routes, { + videoUploadOnPasses: true + }) + + it "warns but proceeds", -> + e2e.exec(@, { + key: "f858a2bc-b469-4e48-be67-0876339ee7e1" + spec: "record_pass*" + record: true + snapshot: true + expectedExitCode: 0 + }) + .then -> + urls = getRequestUrls() + + expect(urls).to.have.members([ + "POST /runs" + "POST /runs/#{runId}/instances" + "PUT /instances/#{instanceId}" + "PUT /videos/video.mp4" + "PUT /screenshots/1.png" + "PUT /instances/#{instanceId}/stdout" + ]) diff --git a/packages/server/test/e2e/reporters_spec.coffee b/packages/server/test/e2e/reporters_spec.coffee index da1c26a12895..747ba746fa29 100644 --- a/packages/server/test/e2e/reporters_spec.coffee +++ b/packages/server/test/e2e/reporters_spec.coffee @@ -1,11 +1,10 @@ -cp = require("child_process") -e2e = require("../support/helpers/e2e") -Fixtures = require("../support/helpers/fixtures") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") +cp = require("child_process") +fs = require("../../lib/util/fs") +e2e = require("../support/helpers/e2e") +Fixtures = require("../support/helpers/fixtures") -fs = Promise.promisifyAll(fs) e2ePath = Fixtures.projectPath("e2e") mochaAwesomes = [ @@ -27,6 +26,15 @@ describe "e2e reporters", -> reporter: "module-does-not-exist" }) + ## https://github.com/cypress-io/cypress/issues/1192 + it "reports error when thrown from reporter", -> + e2e.exec(@, { + spec: "simple_passing_spec.coffee" + snapshot: true + expectedExitCode: 1 + reporter: "reporters/throws.js" + }) + it "supports junit reporter and reporter options", -> e2e.exec(@, { spec: "simple_passing_spec.coffee" @@ -74,7 +82,7 @@ describe "e2e reporters", -> e2e.exec(@, { spec: "simple_failing_hook_spec.coffee" snapshot: true - expectedExitCode: 1 + expectedExitCode: 3 reporter: ma }) .then -> @@ -82,7 +90,7 @@ describe "e2e reporters", -> fs.readFileAsync(path.join(e2ePath, "mochawesome-reports", "mochawesome.html"), "utf8") .then (xml) -> expect(xml).to.include("<h3 class=\"suite-title\">simple failing hook spec</h3>") - expect(xml).to.include("<div class=\"status-item status-item-hooks danger\">1 Failed Hook</div>") + expect(xml).to.include("<div class=\"status-item status-item-hooks danger\">3 Failed Hooks</div>") else fs.readJsonAsync(path.join(e2ePath, "mochawesome-report", "mochawesome.json")) .then (json) -> @@ -90,4 +98,4 @@ describe "e2e reporters", -> ## 'failures' but it does collect them in 'other' expect(json.stats).to.be.an('object') expect(json.stats.failures).to.eq(0) - expect(json.stats.other).to.eq(1) + expect(json.stats.other).to.eq(3) diff --git a/packages/server/test/e2e/screenshot_app_capture_spec.coffee b/packages/server/test/e2e/screenshot_app_capture_spec.coffee new file mode 100644 index 000000000000..18b2a047c11d --- /dev/null +++ b/packages/server/test/e2e/screenshot_app_capture_spec.coffee @@ -0,0 +1,25 @@ +e2e = require("../support/helpers/e2e") + +onServer = (app) -> + app.get "/app", e2e.sendHtml(""" + <div class="black-me-out" style="position: fixed; left: 10px; top: 10px;">Redacted</div> + """) + +describe "e2e screenshot app capture", -> + e2e.setup({ + servers: { + port: 3322 + onServer: onServer + } + }) + + it "passes", -> + ## this tests that consistent screenshots are taken for app + ## captures (namely that the runner UI is hidden) + + e2e.exec(@, { + spec: "screenshot_app_capture_spec.coffee" + expectedExitCode: 0 + snapshot: true + timemout: 180000 + }) diff --git a/packages/server/test/e2e/screenshot_element_capture_spec.coffee b/packages/server/test/e2e/screenshot_element_capture_spec.coffee new file mode 100644 index 000000000000..0619f0d034a1 --- /dev/null +++ b/packages/server/test/e2e/screenshot_element_capture_spec.coffee @@ -0,0 +1,27 @@ +e2e = require("../support/helpers/e2e") + +onServer = (app) -> + app.get "/element", e2e.sendHtml(""" + <style>body { margin: 0; }</style> + <div class="capture-me" style="height: 300px; border: solid 1px black; margin: 20px;"> + <div style="background: black; height: 150px;"></div> + </div> + """) + +describe "e2e screenshot element capture", -> + e2e.setup({ + servers: { + port: 3322 + onServer: onServer + } + }) + + it "passes", -> + ## this tests that consistent screenshots are taken for element captures, + ## that the runner UI is hidden and that the page is scrolled properly + + e2e.exec(@, { + spec: "screenshot_element_capture_spec.coffee" + expectedExitCode: 0 + snapshot: true + }) diff --git a/packages/server/test/e2e/screenshot_fullpage_capture_spec.coffee b/packages/server/test/e2e/screenshot_fullpage_capture_spec.coffee new file mode 100644 index 000000000000..26677be02563 --- /dev/null +++ b/packages/server/test/e2e/screenshot_fullpage_capture_spec.coffee @@ -0,0 +1,29 @@ +e2e = require("../support/helpers/e2e") + +onServer = (app) -> + app.get "/fullPage", e2e.sendHtml(""" + <style>body { margin: 0; }</style> + <div class="black-me-out" style="position: absolute; left: 10px; top: 10px;">Redacted</div> + <div style="background: white; height: 200px;"></div> + <div style="background: black; height: 200px;"></div> + <div style="background: white; height: 100px;"></div> + """) + +describe "e2e screenshot fullPage capture", -> + e2e.setup({ + servers: { + port: 3322 + onServer: onServer + } + }) + + it "passes", -> + ## this tests that consistent screenshots are taken for fullPage captures, + ## that the runner UI is hidden and that the page is scrolled properly + + e2e.exec(@, { + spec: "screenshot_fullpage_capture_spec.coffee" + expectedExitCode: 0 + snapshot: true + timeout: 180000 ## 3 minutes + }) diff --git a/packages/server/test/e2e/screenshots_spec.coffee b/packages/server/test/e2e/screenshots_spec.coffee index c2bdc8243116..1f458fff8452 100644 --- a/packages/server/test/e2e/screenshots_spec.coffee +++ b/packages/server/test/e2e/screenshots_spec.coffee @@ -1,8 +1,8 @@ _ = require("lodash") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") sizeOf = require("image-size") +fs = require("../../lib/util/fs") Fixtures = require("../support/helpers/fixtures") e2e = require("../support/helpers/e2e") @@ -11,20 +11,34 @@ sizeOf = Promise.promisify(sizeOf) e2ePath = Fixtures.projectPath("e2e") onServer = (app) -> - getHtml = (color) -> - """ - <!DOCTYPE html> - <html lang="en"> - <body> - <div style="height: 2000px; width: 2000px; background-color: #{color};"></div> - </body> - </html> - """ - app.get "/color/:color", (req, res) -> - res.set('Content-Type', 'text/html'); + e2e.sendHtml("""<div style="height: 2000px; width: 2000px; background-color: #{req.params.color};"></div>""")(req, res) + + app.get "/fullPage", e2e.sendHtml(""" + <style>body { margin: 0; }</style> + <div style="background: white; height: 200px;"></div> + <div style="background: black; height: 200px;"></div> + <div style="background: white; height: 100px;"></div> + """) + + app.get "/fullPage-same", e2e.sendHtml(""" + <style>body { margin: 0; }</style> + <div style="height: 500px;"></div> + """) + + app.get "/element", e2e.sendHtml(""" + <div class="element" style="background: red; width: 400px; height: 300px; margin: 20px;"></div> + """) - res.send(getHtml(req.params.color)) + app.get "/pathological", e2e.sendHtml(""" + <style>div { width: 1px; height: 1px; position: fixed; }</style> + <div style="left: 0; top: 0; background-color: grey;"></div> + <div style="left: 1px; top: 0; background-color: white;"></div> + <div style="left: 0; top: 1px; background-color: white;"></div> + <div style="right: 0; top: 0; background-color: white;"></div> + <div style="left: 0; bottom: 0; background-color: white;"></div> + <div style="right: 0; bottom: 0; background-color: black;"></div> + """) describe "e2e screenshots", -> e2e.setup({ @@ -41,7 +55,7 @@ describe "e2e screenshots", -> e2e.exec(@, { spec: "screenshots_spec.coffee" - expectedExitCode: 4 + expectedExitCode: 3 snapshot: true }) .then -> diff --git a/packages/server/test/e2e/spec_isolation_spec.coffee b/packages/server/test/e2e/spec_isolation_spec.coffee new file mode 100644 index 000000000000..e429c39ce35e --- /dev/null +++ b/packages/server/test/e2e/spec_isolation_spec.coffee @@ -0,0 +1,228 @@ +_ = require("lodash") +path = require("path") +moment = require("moment") +snapshot = require("snap-shot-it") +fs = require("../../lib/util/fs") +e2e = require("../support/helpers/e2e") +Fixtures = require("../support/helpers/fixtures") + +e2ePath = Fixtures.projectPath("e2e") + +outputPath = path.join(e2ePath, "output.json") + +STATIC_DATE = "2018-02-01T20:14:19.323Z" + +specs = [ + "simple_passing_spec.coffee" + "simple_hooks_spec.coffee" + "simple_failing_spec.coffee" + "simple_failing_h*_spec.coffee" ## simple failing hook spec +].join(",") + +expectStartToBeBeforeEnd = (obj, start, end) -> + s = _.get(obj, start) + e = _.get(obj, end) + + expect( + moment(s).isBefore(e), + "expected start: #{s} to be before end: #{e}" + ).to.be.true + + ## once valid, mutate and set static dates + _.set(obj, start, STATIC_DATE) + _.set(obj, end, STATIC_DATE) + +expectDurationWithin = (obj, duration, low, high, reset) -> + d = _.get(obj, duration) + + ## bail if we don't have a duration + return if not _.isNumber(d) + + ## ensure the duration is within range + expect(d).to.be.within(low, high) + + ## once valid, mutate and set static range + _.set(obj, duration, reset) + +normalizeTestTimings = (obj, timings) -> + t = _.get(obj, timings) + + ## bail if we don't have any timings + return if not t + + _.set obj, "timings", _.mapValues t, (val, key) -> + switch key + when "lifecycle" + ## ensure that lifecycle is under 500ms + expect(val, "lifecycle").to.be.within(0, 500) + + ## reset to 100 + return 100 + when "test" + ## ensure test fn duration is within 1500ms + expectDurationWithin(val, "fnDuration", 0, 1500, 400) + ## ensure test after fn duration is within 500ms + expectDurationWithin(val, "afterFnDuration", 0, 500, 200) + + return val + else + _.map val, (hook) -> + ## ensure test fn duration is within 1500ms + expectDurationWithin(hook, "fnDuration", 0, 1500, 400) + ## ensure test after fn duration is within 500ms + expectDurationWithin(hook, "afterFnDuration", 0, 500, 200) + + return hook + +expectRunsToHaveCorrectStats = (runs = []) -> + runs.forEach (run) -> + expectStartToBeBeforeEnd(run, "stats.wallClockStartedAt", "stats.wallClockEndedAt") + expectStartToBeBeforeEnd(run, "reporterStats.start", "reporterStats.end") + + ## grab all the wallclock durations for all tests + ## because our duration should be at least this + wallClocks = _.sumBy(run.tests, "wallClockDuration") + + ## ensure each run's duration is around the sum + ## of all tests wallclock duration + expectDurationWithin( + run, + "stats.wallClockDuration", + wallClocks, + wallClocks + 150, ## add 150ms to account for padding + 1234 + ) + + expectDurationWithin( + run, + "reporterStats.duration", + wallClocks, + wallClocks + 150, ## add 150ms to account for padding + 1234 + ) + + addFnAndAfterFn = (obj) -> + ## add these two together + obj.fnDuration + obj.afterFnDuration + + run.spec.absolute = e2e.normalizeStdout(run.spec.absolute) + + ## now make sure that each tests wallclock duration + ## is around the sum of all of its timings + run.tests.forEach (test) -> + ## cannot sum an object, must use array of values + timings = _.sumBy _.values(test.timings), (val) -> + switch + when _.isArray(val) + ## array for hooks + _.sumBy(val, addFnAndAfterFn) + when _.isObject(val) + ## obj for test itself + addFnAndAfterFn(val) + else + val + + expectDurationWithin( + test, + "wallClockDuration", + timings, + timings + 50, ## add 50ms to account for padding + 1234 + ) + + ## now reset all the test timings + normalizeTestTimings(test, "timings") + + ## normalize stack + if test.stack + test.stack = e2e.normalizeStdout(test.stack) + + if test.wallClockStartedAt + d = new Date(test.wallClockStartedAt) + expect(d.toJSON()).to.eq(test.wallClockStartedAt) + test.wallClockStartedAt = STATIC_DATE + + expect(test.videoTimestamp).to.be.a("number") + test.videoTimestamp = 9999 + + ## normalize video path + run.video = e2e.normalizeStdout(run.video) + + ## normalize screenshot dynamic props + run.screenshots = _.map run.screenshots, (screenshot) -> + expect(screenshot.screenshotId).to.have.length("5") + + d = new Date(screenshot.takenAt) + expect(d.toJSON()).to.eq(screenshot.takenAt) + screenshot.takenAt = STATIC_DATE + + screenshot.screenshotId = "some-random-id" + screenshot.path = e2e.normalizeStdout(screenshot.path) + screenshot + +describe "e2e spec_isolation", -> + e2e.setup() + + it "failing", -> + e2e.exec(@, { + spec: specs + outputPath: outputPath + snapshot: false + expectedExitCode: 5 + }) + .then -> + ## now what we want to do is read in the outputPath + ## and snapshot it so its what we expect after normalizing it + fs.readJsonAsync(outputPath) + .then (json) -> + ## ensure that config has been set + expect(json.config).to.be.an('object') + expect(json.config.projectName).to.eq("e2e") + expect(json.config.projectRoot).to.eq(e2ePath) + + ## but zero out config because it's too volatile + json.config = {} + + expect(json.browserPath).to.be.a('string') + expect(json.browserName).to.be.a('string') + expect(json.browserVersion).to.be.a('string') + expect(json.osName).to.be.a('string') + expect(json.osVersion).to.be.a('string') + expect(json.cypressVersion).to.be.a('string') + + _.extend(json, { + browserPath: 'path/to/browser' + browserName: 'FooBrowser' + browserVersion: '88' + osName: 'FooOS' + osVersion: '1234' + cypressVersion: '9.9.9' + }) + + ## ensure the totals are accurate + expect(json.totalTests).to.eq( + _.sum([ + json.totalFailed, + json.totalPassed, + json.totalPending, + json.totalSkipped + ]) + ) + + expectStartToBeBeforeEnd(json, "startedTestsAt", "endedTestsAt") + + ## ensure totalDuration matches all of the stats durations + expectDurationWithin( + json, + "totalDuration", + _.sumBy(json.runs, "stats.wallClockDuration"), + _.sumBy(json.runs, "stats.wallClockDuration"), + 5555 + ) + + ## should be 4 total runs + expect(json.runs).to.have.length(4) + + expectRunsToHaveCorrectStats(json.runs) + + snapshot(json) diff --git a/packages/server/test/e2e/specs_spec.coffee b/packages/server/test/e2e/specs_spec.coffee new file mode 100644 index 000000000000..f3eeb1641086 --- /dev/null +++ b/packages/server/test/e2e/specs_spec.coffee @@ -0,0 +1,21 @@ +e2e = require("../support/helpers/e2e") +Fixtures = require("../support/helpers/fixtures") + +e2ePath = Fixtures.projectPath("e2e") + +describe "e2e specs", -> + e2e.setup() + + it "failing when no specs found", -> + e2e.exec(@, { + config: "integrationFolder=cypress/specs" + snapshot: true + expectedExitCode: 1 + }) + + it "failing when no spec pattern found", -> + e2e.exec(@, { + spec: "cypress/integration/**notfound**" + snapshot: true + expectedExitCode: 1 + }) diff --git a/packages/server/test/e2e/stdout_spec.coffee b/packages/server/test/e2e/stdout_spec.coffee index 697a61032ac8..ea967a294cf4 100644 --- a/packages/server/test/e2e/stdout_spec.coffee +++ b/packages/server/test/e2e/stdout_spec.coffee @@ -26,3 +26,19 @@ describe "e2e stdout", -> snapshot: true expectedExitCode: 0 }) + + it "logs that electron cannot be recorded in headed mode", -> + e2e.exec(@, { + spec: "simple_spec.coffee" + headed: true + snapshot: true + expectedExitCode: 0 + }) + + it "logs that chrome cannot be recorded", -> + e2e.exec(@, { + spec: "simple_spec.coffee" + browser: "chrome" + snapshot: true + expectedExitCode: 0 + }) diff --git a/packages/server/test/e2e/task_not_registered_spec.coffee b/packages/server/test/e2e/task_not_registered_spec.coffee new file mode 100644 index 000000000000..b90d058aadd8 --- /dev/null +++ b/packages/server/test/e2e/task_not_registered_spec.coffee @@ -0,0 +1,13 @@ +e2e = require("../support/helpers/e2e") +Fixtures = require("../support/helpers/fixtures") + +describe "e2e task", -> + e2e.setup() + + it "fails", -> + e2e.exec(@, { + project: Fixtures.projectPath("task-not-registered") + spec: "task_not_registered_spec.coffee" + snapshot: true + expectedExitCode: 1 + }) diff --git a/packages/server/test/e2e/task_spec.coffee b/packages/server/test/e2e/task_spec.coffee new file mode 100644 index 000000000000..25d2a95055b6 --- /dev/null +++ b/packages/server/test/e2e/task_spec.coffee @@ -0,0 +1,16 @@ +e2e = require("../support/helpers/e2e") + +describe "e2e task", -> + e2e.setup() + + it "fails", -> + e2e.exec(@, { + spec: "task_spec.coffee" + snapshot: true + expectedExitCode: 2 + }) + .then ({ stdout }) -> + ## should include a stack trace from plugins file + match = stdout.match(/at errors(.*)\n/) + expect(match).not.to.be.null + expect(match[0]).to.include("plugins/index.js") diff --git a/packages/server/test/e2e/websockets_spec.coffee b/packages/server/test/e2e/websockets_spec.coffee new file mode 100644 index 000000000000..0e2f5fd4c67a --- /dev/null +++ b/packages/server/test/e2e/websockets_spec.coffee @@ -0,0 +1,38 @@ +ws = require("ws") + +e2e = require("../support/helpers/e2e") + +onServer = (app) -> + app.get "/foo", (req, res) -> + res.send("<html>foo></html>") + +onWsServer = (app, server) -> + wss = new ws.Server({ server }) + wss.on "connection", (ws) -> + ws.on "message", (msg) -> + ws.send(msg + "bar") + +onWssServer = (app) -> + +describe "e2e websockets", -> + e2e.setup({ + servers: [{ + port: 3038 + static: true + onServer: onServer + }, { + port: 3039 + onServer: onWsServer + }, { + port: 3040 + onServer: onWssServer + }] + }) + + ## https://github.com/cypress-io/cypress/issues/556 + it "passes", -> + e2e.exec(@, { + spec: "websockets_spec.coffee" + snapshot: true + expectedExitCode: 0 + }) diff --git a/packages/server/test/integration/cypress_spec.coffee b/packages/server/test/integration/cypress_spec.coffee index e028abae81e1..bf0c4777354f 100644 --- a/packages/server/test/integration/cypress_spec.coffee +++ b/packages/server/test/integration/cypress_spec.coffee @@ -9,34 +9,37 @@ http = require("http") Promise = require("bluebird") electron = require("electron") commitInfo = require("@cypress/commit-info") +isForkPr = require("is-fork-pr") Fixtures = require("../support/helpers/fixtures") pkg = require("@packages/root") launcher = require("@packages/launcher") extension = require("@packages/extension") +fs = require("#{root}lib/util/fs") connect = require("#{root}lib/util/connect") ciProvider = require("#{root}lib/util/ci_provider") settings = require("#{root}lib/util/settings") Events = require("#{root}lib/gui/events") Windows = require("#{root}lib/gui/windows") record = require("#{root}lib/modes/record") -headed = require("#{root}lib/modes/headed") -headless = require("#{root}lib/modes/headless") +interactiveMode = require("#{root}lib/modes/interactive") +runMode = require("#{root}lib/modes/run") api = require("#{root}lib/api") cwd = require("#{root}lib/cwd") user = require("#{root}lib/user") config = require("#{root}lib/config") cache = require("#{root}lib/cache") -stdout = require("#{root}lib/stdout") +video = require("#{root}lib/video") errors = require("#{root}lib/errors") -upload = require("#{root}lib/upload") +plugins = require("#{root}lib/plugins") cypress = require("#{root}lib/cypress") Project = require("#{root}lib/project") Server = require("#{root}lib/server") Reporter = require("#{root}lib/reporter") -utils = require("#{root}lib/browsers/utils") -browsers = require("#{root}lib/browsers") Watchers = require("#{root}lib/watchers") +browsers = require("#{root}lib/browsers") +browserUtils = require("#{root}lib/browsers/utils") openProject = require("#{root}lib/open_project") +env = require("#{root}lib/util/env") appData = require("#{root}lib/util/app_data") formStatePath = require("#{root}lib/util/saved_state").formStatePath @@ -80,20 +83,23 @@ describe "lib/cypress", -> @todosPath = Fixtures.projectPath("todos") @pristinePath = Fixtures.projectPath("pristine") @noScaffolding = Fixtures.projectPath("no-scaffolding") + @recordPath = Fixtures.projectPath("record") @pluginConfig = Fixtures.projectPath("plugin-config") @pluginBrowser = Fixtures.projectPath("plugin-browser") @idsPath = Fixtures.projectPath("ids") ## force cypress to call directly into main without ## spawning a separate process - @sandbox.stub(cypress, "isCurrentlyRunningElectron").returns(true) - @sandbox.stub(extension, "setHostAndPath").resolves() - @sandbox.stub(launcher, "detect").resolves(TYPICAL_BROWSERS) - @sandbox.stub(process, "exit") - @sandbox.stub(Server.prototype, "reset") - @sandbox.spy(errors, "log") - @sandbox.spy(errors, "warning") - @sandbox.spy(console, "log") + sinon.stub(video, "start").resolves({}) + sinon.stub(plugins, "init").resolves(undefined) + sinon.stub(cypress, "isCurrentlyRunningElectron").returns(true) + sinon.stub(extension, "setHostAndPath").resolves() + sinon.stub(launcher, "detect").resolves(TYPICAL_BROWSERS) + sinon.stub(process, "exit") + sinon.stub(Server.prototype, "reset") + sinon.spy(errors, "log") + sinon.spy(errors, "warning") + sinon.spy(console, "log") @expectExitWith = (code) => expect(process.exit).to.be.calledWith(code) @@ -122,7 +128,7 @@ describe "lib/cypress", -> @projectId = id ]) .then => - @sandbox.stub(api, "getProjectToken") + sinon.stub(api, "getProjectToken") .withArgs(@projectId, "auth-token-123") .resolves("new-key-123") @@ -161,7 +167,7 @@ describe "lib/cypress", -> @projectId = id ]) .then => - @sandbox.stub(api, "getProjectToken") + sinon.stub(api, "getProjectToken") .withArgs(@projectId, "auth-token-123") .rejects(new Error()) @@ -179,7 +185,7 @@ describe "lib/cypress", -> @projectId = id ]) .then => - @sandbox.stub(api, "updateProjectToken") + sinon.stub(api, "updateProjectToken") .withArgs(@projectId, "auth-token-123") .resolves("new-key-123") @@ -218,7 +224,7 @@ describe "lib/cypress", -> @projectId = id ]) .then => - @sandbox.stub(api, "updateProjectToken") + sinon.stub(api, "updateProjectToken") .withArgs(@projectId, "auth-token-123") .rejects(new Error()) @@ -228,28 +234,27 @@ describe "lib/cypress", -> context "--run-project", -> beforeEach -> - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() - @sandbox.stub(headless, "waitForSocketConnection") - @sandbox.stub(headless, "listenForProjectEnd").resolves({failures: 0}) - @sandbox.stub(browsers, "open") - @sandbox.stub(commitInfo, "getRemoteOrigin").resolves("remoteOrigin") + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(runMode, "waitForSocketConnection") + sinon.stub(runMode, "listenForProjectEnd").resolves({stats: {failures: 0} }) + sinon.stub(browsers, "open") + sinon.stub(commitInfo, "getRemoteOrigin").resolves("remoteOrigin") it "runs project headlessly and exits with exit code 0", -> cypress.start(["--run-project=#{@todosPath}"]) .then => - expect(browsers.open).to.be.calledWithMatch("electron", {url: "http://localhost:8888/__/#/tests/__all"}) + expect(browsers.open).to.be.calledWithMatch("electron") @expectExitWith(0) it "runs project headlessly and exits with exit code 10", -> - headless.listenForProjectEnd.resolves({failures: 10}) + sinon.stub(runMode, "runSpecs").resolves({ totalFailed: 10 }) cypress.start(["--run-project=#{@todosPath}"]) .then => - expect(browsers.open).to.be.calledWithMatch("electron", {url: "http://localhost:8888/__/#/tests/__all"}) @expectExitWith(10) it "does not generate a project id even if missing one", -> - @sandbox.stub(api, "createProject") + sinon.stub(api, "createProject") user.set({authToken: "auth-token-123"}) .then => @@ -261,19 +266,19 @@ describe "lib/cypress", -> Project(@noScaffolding).getProjectId() .then -> - throw new Error("should have caught error but didnt") + throw new Error("should have caught error but did not") .catch (err) -> expect(err.type).to.eq("NO_PROJECT_ID") it "does not add project to the global cache", -> - cache.getProjectPaths() + cache.getProjectRoots() .then (projects) => ## no projects in the cache expect(projects.length).to.eq(0) cypress.start(["--run-project=#{@todosPath}"]) .then -> - cache.getProjectPaths() + cache.getProjectRoots() .then (projects) -> ## still not projects expect(projects.length).to.eq(0) @@ -281,9 +286,14 @@ describe "lib/cypress", -> it "runs project by relative spec and exits with status 0", -> relativePath = path.relative(cwd(), @todosPath) - cypress.start(["--run-project=#{@todosPath}", "--spec=#{relativePath}/tests/test2.coffee"]) + cypress.start([ + "--run-project=#{@todosPath}", + "--spec=#{relativePath}/tests/test2.coffee" + ]) .then => - expect(browsers.open).to.be.calledWithMatch("electron", {url: "http://localhost:8888/__/#/tests/integration/test2.coffee"}) + expect(browsers.open).to.be.calledWithMatch("electron", { + url: "http://localhost:8888/__/#/tests/integration/test2.coffee" + }) @expectExitWith(0) it "runs project by specific spec with default configuration", -> @@ -298,18 +308,22 @@ describe "lib/cypress", -> expect(browsers.open).to.be.calledWithMatch("electron", {url: "http://localhost:8888/__/#/tests/integration/test2.coffee"}) @expectExitWith(0) - it "scaffolds out integration and example_spec if they do not exist when not headless", -> + it "scaffolds out integration and example specs if they do not exist when not runMode", -> config.get(@pristinePath) .then (cfg) => fs.statAsync(cfg.integrationFolder) .then -> throw new Error("integrationFolder should not exist!") .catch => - cypress.start(["--run-project=#{@pristinePath}", "--no-headless"]) + cypress.start(["--run-project=#{@pristinePath}", "--no-runMode"]) .then => fs.statAsync(cfg.integrationFolder) .then => - fs.statAsync path.join(cfg.integrationFolder, "example_spec.js") + Promise.join( + fs.statAsync(path.join(cfg.integrationFolder, "examples", "actions.spec.js")), + fs.statAsync(path.join(cfg.integrationFolder, "examples", "files.spec.js")), + fs.statAsync(path.join(cfg.integrationFolder, "examples", "viewport.spec.js")) + ) it "does not scaffold when headless and exits with error when no existing project", -> ensureDoesNotExist = (inspection, index) -> @@ -334,7 +348,7 @@ describe "lib/cypress", -> .then => @expectExitWithErr("PROJECT_DOES_NOT_EXIST", @pristinePath) - it "does not scaffold integration or example_spec when headless", -> + it "does not scaffold integration or example specs when runMode", -> settings.write(@pristinePath, {}) .then => cypress.start(["--run-project=#{@pristinePath}"]) @@ -351,7 +365,7 @@ describe "lib/cypress", -> .then -> throw new Error("fixturesFolder should not exist!") .catch => - cypress.start(["--run-project=#{@pristinePath}", "--no-headless"]) + cypress.start(["--run-project=#{@pristinePath}", "--no-runMode"]) .then => fs.statAsync(cfg.fixturesFolder) .then => @@ -366,7 +380,7 @@ describe "lib/cypress", -> .then -> throw new Error("supportFolder should not exist!") .catch {code: "ENOENT"}, => - cypress.start(["--run-project=#{@pristinePath}", "--no-headless"]) + cypress.start(["--run-project=#{@pristinePath}", "--no-runMode"]) .then => fs.statAsync(supportFolder) .then => @@ -395,14 +409,13 @@ describe "lib/cypress", -> cypress.start(["--run-project=#{@todosPath}", "--headed"]) .then => expect(browsers.open).to.be.calledWithMatch("electron", { - url: "http://localhost:8888/__/#/tests/__all" proxyServer: "http://localhost:8888" show: true }) @expectExitWith(0) it "turns on reporting", -> - @sandbox.spy(Reporter, "create") + sinon.spy(Reporter, "create") cypress.start(["--run-project=#{@todosPath}"]) .then => @@ -410,7 +423,7 @@ describe "lib/cypress", -> @expectExitWith(0) it "can change the reporter to nyan", -> - @sandbox.spy(Reporter, "create") + sinon.spy(Reporter, "create") cypress.start(["--run-project=#{@todosPath}", "--reporter=nyan"]) .then => @@ -418,7 +431,7 @@ describe "lib/cypress", -> @expectExitWith(0) it "can change the reporter with cypress.json", -> - @sandbox.spy(Reporter, "create") + sinon.spy(Reporter, "create") config.get(@idsPath) .then (@cfg) => @@ -459,34 +472,6 @@ describe "lib/cypress", -> expect(errors.warning).not.to.be.calledWith("PROJECT_ID_AND_KEY_BUT_MISSING_RECORD_OPTION", "abc123") expect(console.log).not.to.be.calledWithMatch("cypress run --key <record_key>") - it "writes json results when passed outputPath", -> - obj = { - tests: 1 - passes: 2 - pending: 3 - failures: 4 - duration: 5 - video: 6 - version: 7 - screenshots: [] - } - - outputPath = "./.results/results.json" - - headless.listenForProjectEnd.resolves(_.clone(obj)) - - cypress.start(["--run-project=#{@todosPath}", "--output-path=#{outputPath}"]) - .then => - @expectExitWith(4) - - fs.readJsonAsync(cwd(outputPath)) - .then (json) -> - expect(json).to.deep.eq( - headless.collectTestResults(obj) - ) - .finally => - fs.removeAsync(cwd(path.dirname(outputPath))) - it "logs error when supportFile doesn't exist", -> settings.write(@idsPath, {supportFile: "/does/not/exist"}) .then => @@ -523,12 +508,25 @@ describe "lib/cypress", -> it "logs error and exits when spec file was specified and does not exist", -> cypress.start(["--run-project=#{@todosPath}", "--spec=path/to/spec"]) .then => - @expectExitWithErr("SPEC_FILE_NOT_FOUND", "#{cwd()}/path/to/spec") + @expectExitWithErr("NO_SPECS_FOUND", "path/to/spec") + @expectExitWithErr("NO_SPECS_FOUND", "We searched for any files matching this glob pattern:") it "logs error and exits when spec absolute file was specified and does not exist", -> - cypress.start(["--run-project=#{@todosPath}", "--spec=#{@todosPath}/tests/path/to/spec"]) + cypress.start([ + "--run-project=#{@todosPath}", + "--spec=#{@todosPath}/tests/path/to/spec" + ]) + .then => + @expectExitWithErr("NO_SPECS_FOUND", "tests/path/to/spec") + + it "logs error and exits when no specs were found at all", -> + cypress.start([ + "--run-project=#{@todosPath}", + "--config=integrationFolder=cypress/specs" + ]) .then => - @expectExitWithErr("SPEC_FILE_NOT_FOUND", "#{@todosPath}/tests/path/to/spec") + @expectExitWithErr("NO_SPECS_FOUND", "We searched for any files inside of this folder:") + @expectExitWithErr("NO_SPECS_FOUND", "cypress/specs") it "logs error and exits when project has cypress.json syntax error", -> fs.writeFileAsync(@todosPath + "/cypress.json", "{'foo': 'bar}") @@ -643,6 +641,8 @@ describe "lib/cypress", -> @expectExitWith(0) it "can override values in plugins", -> + plugins.init.restore() + cypress.start([ "--run-project=#{@pluginConfig}", "--config=requestTimeout=1234,videoCompression=false" "--env=foo=foo,bar=bar" @@ -678,6 +678,7 @@ describe "lib/cypress", -> describe "plugins", -> beforeEach -> + plugins.init.restore() browsers.open.restore() ee = new EE() @@ -689,13 +690,13 @@ describe "lib/cypress", -> ee.loadURL = -> ee.webContents = { session: { - clearCache: @sandbox.stub().yieldsAsync() + clearCache: sinon.stub().yieldsAsync() } } - @sandbox.stub(utils, "launch").resolves(ee) - @sandbox.stub(Windows, "create").returns(ee) - @sandbox.stub(Windows, "automation") + sinon.stub(browserUtils, "launch").resolves(ee) + sinon.stub(Windows, "create").returns(ee) + sinon.stub(Windows, "automation") context "before:browser:launch", -> it "chrome", -> @@ -704,7 +705,7 @@ describe "lib/cypress", -> "--browser=chrome" ]) .then => - args = utils.launch.firstCall.args + args = browserUtils.launch.firstCall.args expect(args[0]).to.eq("chrome") @@ -733,11 +734,11 @@ describe "lib/cypress", -> describe "--port", -> beforeEach -> - headless.listenForProjectEnd.resolves({failures: 0}) + runMode.listenForProjectEnd.resolves({stats: {failures: 0} }) it "can change the default port to 5555", -> - listen = @sandbox.spy(http.Server.prototype, "listen") - open = @sandbox.spy(Server.prototype, "open") + listen = sinon.spy(http.Server.prototype, "listen") + open = sinon.spy(Server.prototype, "open") cypress.start(["--run-project=#{@todosPath}", "--port=5555"]) .then => @@ -763,7 +764,7 @@ describe "lib/cypress", -> process.env = _.omit(process.env, "CYPRESS_DEBUG") - headless.listenForProjectEnd.resolves({failures: 0}) + runMode.listenForProjectEnd.resolves({stats: {failures: 0} }) afterEach -> process.env = @env @@ -785,71 +786,30 @@ describe "lib/cypress", -> @expectExitWith(0) - ## the majority of the logic in Record mode is covered already - ## in --run-project specs above + ## most record mode logic is covered in e2e tests. + ## we only need to cover the edge cases / warnings context "--record or --ci", -> - afterEach -> - delete process.env.CYPRESS_PROJECT_ID - delete process.env.CYPRESS_RECORD_KEY - beforeEach -> - @setup = (specPattern, specFiles) => - if not specFiles - specFiles = ["a-spec.js", "b-spec.js"] - - @sandbox.stub(Project, "findSpecs").resolves(specFiles) - - createRunArgs = { - projectId: @projectId - recordKey: "token-123" - commitSha: "sha-123" - commitBranch: "bem/ci" - commitAuthorName: "brian" - commitAuthorEmail: "brian@cypress.io" - commitMessage: "foo" - remoteOrigin: "https://github.com/foo/bar.git" - ciProvider: "travis" - ciBuildNumber: "987" - ciParams: null - groupId: null - specs: specFiles - specPattern: specPattern + sinon.stub(api, "createRun").resolves() + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(browsers, "open") + sinon.stub(runMode, "waitForSocketConnection") + sinon.stub(runMode, "waitForTestsToFinishRunning").resolves({ + stats: { + tests: 1 + passes: 2 + failures: 3 + pending: 4 + skipped: 5 + wallClockDuration: 6 } - - @createRun = @sandbox.stub(api, "createRun").withArgs(createRunArgs) - - @sandbox.stub(upload, "send").resolves() - @sandbox.stub(stdout, "capture").returns({ - toString: -> "foobarbaz" - }) - - @sandbox.stub(ciProvider, "name").returns("travis") - @sandbox.stub(ciProvider, "buildNum").returns("987") - @sandbox.stub(ciProvider, "params").returns(null) - @sandbox.stub(os, "platform").returns("linux") - ## TODO: might need to change this to a different return - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() - @sandbox.stub(commitInfo, "commitInfo").resolves({ - branch: "bem/ci", - sha: "sha-123", - author: "brian", - email: "brian@cypress.io", - message: "foo", - remote: "https://github.com/foo/bar.git" - }) - @sandbox.stub(browsers, "open") - @sandbox.stub(headless, "waitForSocketConnection") - @sandbox.stub(headless, "waitForTestsToFinishRunning").resolves({ - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - video: true + tests: [] + hooks: [] + video: "path/to/video" shouldUploadVideo: true screenshots: [] - failingTests: [] config: {} + spec: {} }) Promise.all([ @@ -861,205 +821,70 @@ describe "lib/cypress", -> @projectId = id ]) - it "runs project in ci and exits with number of failures", -> - @setup() - - @createRun.resolves("build-id-123") - - @createInstance = @sandbox.stub(api, "createInstance").withArgs({ - buildId: "build-id-123" - browser: "electron" - spec: undefined - }).resolves("instance-id-123") - - @updateInstance = @sandbox.stub(api, "updateInstance").withArgs({ - instanceId: "instance-id-123" - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - video: true - error: undefined - screenshots: [] - failingTests: [] - cypressConfig: {} - ciProvider: "travis" - stdout: "foobarbaz" - }).resolves({ - videoUploadUrl: "http://video.url" - }) - - cypress.start(["--run-project=#{@todosPath}", "--record", "--key=token-123"]) - .then => - expect(@createInstance).to.be.calledOnce - expect(@updateInstance).to.be.calledOnce - - expect(upload.send).to.be.calledOnce - - @expectExitWith(3) - - it "sends specs and runs project by specific absolute spec and exits with status 3", -> - spec = "#{@todosPath}/tests/*" - - @setup(spec, [ - "test1.js" - "test2.coffee" - ]) - - ## TODO: fix this once we implement proper globbing - ## per spec. currently just hacking this and forcing - ## it to return something we specify - @sandbox.stub(Project.prototype, "ensureSpecExists").resolves("#{@todosPath}/test2.coffee") - - @createRun.resolves("build-id-123") - - @sandbox.stub(api, "createInstance").withArgs({ - buildId: "build-id-123" - browser: "chrome" - spec: spec - }).resolves("instance-id-123") - - @updateInstance = @sandbox.stub(api, "updateInstance").resolves() - - cypress.start(["--run-project=#{@todosPath}", "--record", "--key=token-123", "--spec=#{spec}", "--browser=chrome"]) - .then => - expect(browsers.open).to.be.calledWithMatch("chrome", {url: "http://localhost:8888/__/#/tests/test2.coffee"}) - @expectExitWith(3) - it "uses process.env.CYPRESS_PROJECT_ID", -> - @setup() + sinon.stub(env, "get").withArgs("CYPRESS_PROJECT_ID").returns(@projectId) - ## set the projectId to be todos even though - ## we are running the pristine project - process.env.CYPRESS_PROJECT_ID = @projectId - - @createRun.resolves() - @sandbox.stub(api, "createInstance").resolves() - - cypress.start(["--run-project=#{@pristinePath}", "--record", "--key=token-123"]) + cypress.start([ + "--run-project=#{@noScaffolding}", + "--record", + "--key=token-123" + ]) .then => + expect(api.createRun).to.be.calledWithMatch({projectId: @projectId}) expect(errors.warning).not.to.be.called @expectExitWith(3) it "uses process.env.CYPRESS_RECORD_KEY", -> - @setup() + sinon.stub(env, "get") + .withArgs("CYPRESS_PROJECT_ID").returns("foo-project-123") + .withArgs("CYPRESS_RECORD_KEY").returns("token") - process.env.CYPRESS_RECORD_KEY = "token-123" - - @createRun.resolves() - @sandbox.stub(api, "createInstance").resolves() - - cypress.start(["--run-project=#{@todosPath}", "--record"]) + cypress.start([ + "--run-project=#{@noScaffolding}", + "--record" + ]) .then => + expect(api.createRun).to.be.calledWithMatch({ + projectId: "foo-project-123" + recordKey: "token" + }) expect(errors.warning).not.to.be.called @expectExitWith(3) - it "still records even with old --ci option", -> - @setup() - - @createRun.resolves("build-id-123") - @sandbox.stub(api, "createInstance").resolves() - @sandbox.stub(api, "updateInstance").resolves() - - cypress.start(["--run-project=#{@todosPath}", "--key=token-123", "--ci"]) - .then => - @expectExitWith(3) - it "logs warning when using deprecated --ci arg and no env var", -> - @setup() - - @createRun.resolves("build-id-123") - @sandbox.stub(api, "createInstance").resolves() - @sandbox.stub(api, "updateInstance").resolves() - - cypress.start(["--run-project=#{@todosPath}", "--key=token-123", "--ci"]) + cypress.start([ + "--run-project=#{@recordPath}", + "--key=token-123", + "--ci" + ]) .then => expect(errors.warning).to.be.calledWith("CYPRESS_CI_DEPRECATED") expect(console.log).to.be.calledWithMatch("You are using the deprecated command:") expect(console.log).to.be.calledWithMatch("cypress run --record --key <record_key>") - - it "logs ONLY CLI warning when using older version of CLI when using deprecated --ci", -> - @setup() - - @createRun.resolves("build-id-123") - @sandbox.stub(api, "createInstance").resolves() - @sandbox.stub(api, "updateInstance").resolves() - - cypress.start(["--run-project=#{@todosPath}", "--key=token-123", "--ci"]) - .then => - expect(errors.warning).to.be.calledWith("CYPRESS_CI_DEPRECATED") expect(errors.warning).not.to.be.calledWith("PROJECT_ID_AND_KEY_BUT_MISSING_RECORD_OPTION") + @expectExitWith(3) it "logs warning when using deprecated --ci arg and env var", -> - @setup() - - process.env.CYPRESS_CI_KEY = "asdf123foobarbaz" - - @createRun.resolves("build-id-123") - @sandbox.stub(api, "createInstance").resolves() - @sandbox.stub(api, "updateInstance").resolves() - - cypress.start(["--run-project=#{@todosPath}", "--key=token-123", "--ci"]) + sinon.stub(env, "get") + .withArgs("CYPRESS_CI_KEY") + .returns("asdf123foobarbaz") + + cypress.start([ + "--run-project=#{@recordPath}", + "--key=token-123", + "--ci" + ]) .then => - delete process.env.CYPRESS_CI_KEY - expect(errors.warning).to.be.calledWith("CYPRESS_CI_DEPRECATED_ENV_VAR") expect(console.log).to.be.calledWithMatch("You are using the deprecated command:") expect(console.log).to.be.calledWithMatch("cypress ci") expect(console.log).to.be.calledWithMatch("cypress run --record") - - it "logs error when missing project id", -> - @setup() - - cypress.start(["--run-project=#{@pristinePath}", "--record", "--key=token-123"]) - .then => - @expectExitWithErr("CANNOT_RECORD_NO_PROJECT_ID") - - it "logs error and exits when ci key is not valid", -> - @setup() - - err = new Error() - err.statusCode = 401 - @createRun.rejects(err) - - cypress.start(["--run-project=#{@todosPath}", "--record", "--key=token-123"]) - .then => - @expectExitWithErr("RECORD_KEY_NOT_VALID", "token...n-123") - - it "logs error and exits when project could not be found", -> - @setup() - - err = new Error() - err.statusCode = 404 - @createRun.rejects(err) - - cypress.start(["--run-project=#{@todosPath}", "--record", "--key=token-123"]) - .then => - @expectExitWithErr("DASHBOARD_PROJECT_NOT_FOUND", "abc123") - - it "logs error but continues running the tests", -> - @setup() - - err = new Error() - err.statusCode = 500 - @createRun.rejects(err) - - cypress.start(["--run-project=#{@todosPath}", "--record", "--key=token-123"]) - .then => @expectExitWith(3) - it "throws when no Record Key was provided", -> - @setup() - - cypress.start(["--run-project=#{@todosPath}", "--record"]) - .then => - @expectExitWithErr("RECORD_KEY_MISSING", "cypress run --record --key <record_key>") - context "--return-pkg", -> beforeEach -> console.log.restore() - @sandbox.stub(console, "log") + sinon.stub(console, "log") it "logs package.json and exits", -> cypress.start(["--return-pkg"]) @@ -1070,7 +895,7 @@ describe "lib/cypress", -> context "--version", -> beforeEach -> console.log.restore() - @sandbox.stub(console, "log") + sinon.stub(console, "log") it "logs version and exits", -> cypress.start(["--version"]) @@ -1081,7 +906,7 @@ describe "lib/cypress", -> context "--smoke-test", -> beforeEach -> console.log.restore() - @sandbox.stub(console, "log") + sinon.stub(console, "log") it "logs pong value and exits", -> cypress.start(["--smoke-test", "--ping=abc123"]) @@ -1089,34 +914,20 @@ describe "lib/cypress", -> expect(console.log).to.be.calledWith("abc123") @expectExitWith(0) - context "--remove-ids", -> - it "logs stats", -> - idsPath = Fixtures.projectPath("ids") - - cypress.start(["--remove-ids", "--run-project=#{idsPath}"]) - .then => - expect(console.log).to.be.calledWith("Removed '5' ids from '2' files.") - @expectExitWith(0) - - it "catches errors when project is not found", -> - cypress.start(["--remove-ids", "--run-project=path/to/no/project"]) - .then => - @expectExitWithErr("NO_PROJECT_FOUND_AT_PROJECT_ROOT", "path/to/no/project") - - context "headed", -> + context "interactive", -> beforeEach -> @win = { - on: @sandbox.stub() + on: sinon.stub() webContents: { - on: @sandbox.stub() + on: sinon.stub() } } - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() - @sandbox.stub(Windows, "open").resolves(@win) - @sandbox.stub(Server.prototype, "startWebsockets") - @sandbox.spy(Events, "start") - @sandbox.stub(electron.ipcMain, "on") + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(Windows, "open").resolves(@win) + sinon.stub(Server.prototype, "startWebsockets") + sinon.spy(Events, "start") + sinon.stub(electron.ipcMain, "on") afterEach -> delete process.env.CYPRESS_FILE_SERVER_FOLDER @@ -1125,12 +936,12 @@ describe "lib/cypress", -> delete process.env.CYPRESS_responseTimeout delete process.env.CYPRESS_watch_for_file_changes - it "passes options to headed.ready", -> - @sandbox.stub(headed, "ready") + it "passes options to interactiveMode.ready", -> + sinon.stub(interactiveMode, "ready") cypress.start(["--updating", "--port=2121", "--config=pageLoadTimeout=1000"]) .then -> - expect(headed.ready).to.be.calledWithMatch({ + expect(interactiveMode.ready).to.be.calledWithMatch({ updating: true config: { port: 2121 @@ -1150,8 +961,8 @@ describe "lib/cypress", -> }) it "passes filtered options to Project#open and sets cli config", -> - getConfig = @sandbox.spy(Project.prototype, "getConfig") - open = @sandbox.stub(Server.prototype, "open").resolves([]) + getConfig = sinon.spy(Project.prototype, "getConfig") + open = sinon.stub(Server.prototype, "open").resolves([]) process.env.CYPRESS_FILE_SERVER_FOLDER = "foo" process.env.CYPRESS_BASE_URL = "localhost" @@ -1225,9 +1036,9 @@ describe "lib/cypress", -> it "sends warning when baseUrl cannot be verified", -> bus = new EE() - event = { sender: { send: @sandbox.stub() } } + event = { sender: { send: sinon.stub() } } warning = { message: "Blah blah baseUrl blah blah" } - open = @sandbox.stub(Server.prototype, "open").resolves([2121, warning]) + open = sinon.stub(Server.prototype, "open").resolves([2121, warning]) cypress.start(["--port=2121", "--config", "pageLoadTimeout=1000", "--foo=bar", "--env=baz=baz"]) .then => @@ -1239,9 +1050,9 @@ describe "lib/cypress", -> context "no args", -> beforeEach -> - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() - @sandbox.stub(headed, "ready").resolves() + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(interactiveMode, "ready").resolves() - it "runs headed and does not exit", -> + it "runs interactiveMode and does not exit", -> cypress.start().then -> - expect(headed.ready).to.be.calledOnce + expect(interactiveMode.ready).to.be.calledOnce diff --git a/packages/server/test/integration/http_requests_spec.coffee b/packages/server/test/integration/http_requests_spec.coffee index 737572f0d9c9..0168f8b7edd7 100644 --- a/packages/server/test/integration/http_requests_spec.coffee +++ b/packages/server/test/integration/http_requests_spec.coffee @@ -1,13 +1,9 @@ require("../spec_helper") -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" - _ = require("lodash") -fs = require("fs-extra") rp = require("request-promise") dns = require("dns") http = require("http") -glob = require("glob") path = require("path") str = require("underscore.string") browserify = require("browserify") @@ -21,19 +17,17 @@ pkg = require("@packages/root") config = require("#{root}lib/config") Server = require("#{root}lib/server") Watchers = require("#{root}lib/watchers") +errors = require("#{root}lib/errors") files = require("#{root}lib/controllers/files") +preprocessor = require("#{root}lib/plugins/preprocessor") CacheBuster = require("#{root}lib/util/cache_buster") +fs = require("#{root}lib/util/fs") +glob = require("#{root}lib/util/glob") Fixtures = require("#{root}test/support/helpers/fixtures") -errors = require("#{root}lib/errors") -preprocessor = require("#{root}lib/plugins/preprocessor") - -fs = Promise.promisifyAll(fs) ## force supertest-session to use supertest-as-promised, hah Session = proxyquire("supertest-session", {supertest: supertest}) -glob = Promise.promisify(glob) - removeWhitespace = (c) -> c = str.clean(c) c = str.lines(c).join(" ") @@ -52,8 +46,10 @@ browserifyFile = (filePath) -> describe "Routes", -> beforeEach -> - @sandbox.stub(CacheBuster, "get").returns("-123") - @sandbox.stub(Server.prototype, "reset") + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" + + sinon.stub(CacheBuster, "get").returns("-123") + sinon.stub(Server.prototype, "reset") nock.enableNetConnect() @@ -509,7 +505,7 @@ describe "Routes", -> describe "delay", -> it "can set delay to 10ms", -> - delay = @sandbox.spy(Promise, "delay") + delay = sinon.spy(Promise, "delay") @rp({ url: "http://localhost:2020/__cypress/xhrs/users/1" @@ -522,7 +518,7 @@ describe "Routes", -> expect(delay).to.be.calledWith(10) it "does not call Promise.delay when no delay", -> - delay = @sandbox.spy(Promise, "delay") + delay = sinon.spy(Promise, "delay") @rp("http://localhost:2020/__cypress/xhrs/users/1") .then (res) -> diff --git a/packages/server/test/integration/server_spec.coffee b/packages/server/test/integration/server_spec.coffee index 09120b01cb88..030d7874d11b 100644 --- a/packages/server/test/integration/server_spec.coffee +++ b/packages/server/test/integration/server_spec.coffee @@ -12,13 +12,15 @@ Fixtures = require("#{root}test/support/helpers/fixtures") describe "Server", -> beforeEach -> - @sandbox.stub(Server.prototype, "reset") + sinon.stub(Server.prototype, "reset") context "resolving url", -> beforeEach -> + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" + nock.enableNetConnect() - @automationRequest = @sandbox.stub() + @automationRequest = sinon.stub() .withArgs("get:cookies").resolves([]) .withArgs("set:cookie").resolves({}) @@ -151,7 +153,7 @@ describe "Server", -> }) it "buffers the response", -> - @sandbox.spy(@server._request, "sendStream") + sinon.spy(@server._request, "sendStream") @server._onResolveUrl("/index.html", {}, @automationRequest) .then (obj = {}) => @@ -394,7 +396,7 @@ describe "Server", -> }) it "buffers the http response", -> - @sandbox.spy(@server._request, "sendStream") + sinon.spy(@server._request, "sendStream") nock("http://espn.com") .get("/") @@ -459,7 +461,7 @@ describe "Server", -> expect(buffers.keys()).to.deep.eq([]) it "does not buffer 'bad' responses", -> - @sandbox.spy(@server._request, "sendStream") + sinon.spy(@server._request, "sendStream") nock("http://espn.com") .get("/") diff --git a/packages/server/test/integration/websockets_spec.coffee b/packages/server/test/integration/websockets_spec.coffee index 7a7463993a35..7668c5df4dcf 100644 --- a/packages/server/test/integration/websockets_spec.coffee +++ b/packages/server/test/integration/websockets_spec.coffee @@ -59,6 +59,23 @@ describe "Web Sockets", -> expect(err.code).to.eq("ECONNRESET") done() + it "sends back 502 Bad Gateway when error upgrading", (done) -> + agent = new httpsAgent("http://localhost:#{cyPort}") + + @server._onDomainSet("http://localhost:#{otherPort}") + + client = new ws("ws://localhost:#{otherPort}", { + agent: agent + }) + + client.on "unexpected-response", (req, res) -> + expect(res.statusCode).to.eq(502) + expect(res.statusMessage).to.eq("Bad Gateway") + expect(res.headers).to.have.property("x-cypress-proxy-error-message") + expect(res.headers).to.have.property("x-cypress-proxy-error-code") + + done() + it "proxies https messages", (done) -> @server._onDomainSet("https://localhost:#{wssPort}") diff --git a/packages/server/test/scripts/e2e.js b/packages/server/test/scripts/e2e.js index 530a215ccc88..86b17f049598 100644 --- a/packages/server/test/scripts/e2e.js +++ b/packages/server/test/scripts/e2e.js @@ -7,10 +7,8 @@ const cp = require('child_process') const minimist = require('minimist') const Promise = require('bluebird') const terminalBanner = require('terminal-banner').terminalBanner - -const humanTime = require('../../lib/util/human_time.coffee') - -const glob = Promise.promisify(require('glob')) +const glob = require('../../lib/util/glob') +const humanTime = require('../../lib/util/human_time') const options = minimist(process.argv.slice(2)) @@ -77,7 +75,7 @@ glob('test/e2e/**/*') .then(() => { const duration = new Date() - started - console.log('Total duration:', humanTime(duration)) + console.log('Total duration:', humanTime.long(duration)) console.log('Exiting with final code:', numFailed) process.exit(numFailed) diff --git a/packages/server/test/spec_helper.coffee b/packages/server/test/spec_helper.coffee index 9d0f08f82b05..33d7fab43726 100644 --- a/packages/server/test/spec_helper.coffee +++ b/packages/server/test/spec_helper.coffee @@ -3,23 +3,37 @@ require("../lib/environment") global.root = "../../" global.supertest = require("supertest-as-promised") global.nock = require("nock") -global.fs = require("fs-extra") global.expect = require("chai").expect global.mockery = require("mockery") global.proxyquire = require("proxyquire") +global.sinon = require("sinon") +_ = require("lodash") Promise = require("bluebird") path = require("path") -sinon = require("sinon") -sinonPromise = require("sinon-as-promised")(Promise) cache = require("../lib/cache") appData = require("../lib/util/app_data") - -global.fs = fs = Promise.promisifyAll(global.fs) - -agent = require("superagent") +agent = require("superagent") require("chai") .use(require("@cypress/sinon-chai")) +.use(require("chai-uuid")) + +env = _.clone(process.env) + +sinon.usingPromise(Promise) + +## backup these originals +restore = sinon.restore +useFakeTimers = sinon.useFakeTimers + +sinon.useFakeTimers = -> + sinon._clock = useFakeTimers.apply(sinon, arguments) + +sinon.restore = -> + if c = sinon._clock + c.restore() + + restore.apply(sinon, arguments) mockery.enable({ warnOnUnregistered: false @@ -29,7 +43,10 @@ mockery.enable({ ## we must use an absolute path here because of the way mockery internally loads this ## module - meaning the first time electron is required it'll use this path string ## so because its required from a separate module we must use an absolute reference to it -mockery.registerSubstitute("electron", path.join(__dirname, "./support/helpers/electron_stub")) +mockery.registerSubstitute( + "electron", + path.join(__dirname, "./support/helpers/electron_stub") +) ## stub out electron's original-fs module which is available when running in electron mockery.registerMock("original-fs", {}) @@ -38,23 +55,17 @@ before -> appData.ensure() beforeEach -> - if global.fs isnt fs - global.fs = fs - nock.disableNetConnect() nock.enableNetConnect(/localhost/) - @sandbox = sinon.sandbox.create() + ## always clean up the cache + ## before each test + cache.remove() afterEach -> - @sandbox.restore() + sinon.restore() nock.cleanAll() nock.enableNetConnect() - if global.fs isnt fs - global.fs = fs - - ## always clean up the cache - ## after each test - cache.remove() + process.env = _.clone(env) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_error_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_error_spec.coffee new file mode 100644 index 000000000000..587f9ec62690 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_error_spec.coffee @@ -0,0 +1 @@ +require("../it/does/not/exist") diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_fail_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_fail_spec.coffee new file mode 100644 index 000000000000..8f4222c9c34c --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_fail_spec.coffee @@ -0,0 +1,7 @@ +describe "record fails", -> + beforeEach -> + throw new Error("foo") + + it "fails 1", -> + + it "is skipped", -> diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_pass_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_pass_spec.coffee new file mode 100644 index 000000000000..0d32f4bac416 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_pass_spec.coffee @@ -0,0 +1,9 @@ +describe "record pass", -> + it "passes", -> + cy.visit("/scrollable.html") + cy + .viewport(400, 400) + .get("#box") + .screenshot('yay it passes') + + it "is pending" diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_uncaught_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_uncaught_spec.coffee new file mode 100644 index 000000000000..34a0ca083278 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/record_uncaught_spec.coffee @@ -0,0 +1 @@ +throw new Error('instantly fails') diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_app_capture_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_app_capture_spec.coffee new file mode 100644 index 000000000000..430125bdb395 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_app_capture_spec.coffee @@ -0,0 +1,14 @@ +it "takes consistent app captures", -> + options = { capture: "viewport", blackout: [".black-me-out"] } + + cy + .visit('http://localhost:3322/app') + .screenshot("app-original", options) + .then -> + ## take 50 screenshots and check that they're all the same + ## to ensure the Cypress UI is consistently hidden + fn = -> + cy.screenshot("app-compare", options) + cy.task("compare:screenshots", { a: 'app-original', b: 'app-compare', blackout: true }) + + Cypress.Promise.map(Cypress._.times(50), fn, { concurrency: 1 }) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_element_capture_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_element_capture_spec.coffee new file mode 100644 index 000000000000..9a6fcbf6f1b2 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_element_capture_spec.coffee @@ -0,0 +1,14 @@ +it "takes consistent element captures", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/element') + .get(".capture-me") + .screenshot("element-original") + .then -> + ## take 10 screenshots and check that they're all the same + ## to ensure element screenshots are consistent + fn = (index) -> + cy.get(".capture-me").screenshot("element-compare") + cy.task("compare:screenshots", { a: 'element-original', b: 'element-compare' }) + + Cypress.Promise.map(Cypress._.times(10), fn, { concurrency: 1 }) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_fullpage_capture_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_fullpage_capture_spec.coffee new file mode 100644 index 000000000000..0016892b3fac --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshot_fullpage_capture_spec.coffee @@ -0,0 +1,15 @@ +it "takes consistent fullPage captures", -> + options = { capture: "fullPage", blackout: [".black-me-out"] } + + cy + .viewport(600, 200) + .visit('http://localhost:3322/fullPage') + .screenshot("fullPage-original", options) + .then -> + ## take 10 screenshots and check that they're all the same + ## to ensure fullPage screenshots are consistent + fn = (index) -> + cy.screenshot("fullPage-compare", options) + cy.task("compare:screenshots", { a: 'fullPage-original', b: 'fullPage-compare', blackout: true }) + + Cypress.Promise.map(Cypress._.times(10), fn, { concurrency: 1 }) diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee index 4416ba763ebe..9a8093b202a2 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/screenshots_spec.coffee @@ -2,15 +2,15 @@ describe "taking screenshots", -> it "manually generates pngs", -> cy .visit('http://localhost:3322/color/black') - .screenshot("black") + .screenshot("black", { capture: "runner" }) .wait(1500) .visit('http://localhost:3322/color/red') - .screenshot("red") + .screenshot("red", { capture: "runner" }) it "can nest screenshots in folders", -> cy .visit('http://localhost:3322/color/white') - .screenshot("foo/bar/baz") + .screenshot("foo/bar/baz", { capture: "runner" }) it "generates pngs on failure", -> cy @@ -20,6 +20,71 @@ describe "taking screenshots", -> ## failure 1 throw new Error("fail whale") + it "crops app captures to just app size", -> + cy + .viewport(600, 400) + .visit('http://localhost:3322/color/yellow') + .screenshot("crop-check", { capture: "viewport" }) + .task("check:screenshot:size", { name: 'crop-check.png', width: 600, height: 400 }) + + it "can capture fullPage screenshots", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/fullPage') + .screenshot("fullPage", { capture: "fullPage" }) + .task("check:screenshot:size", { name: 'fullPage.png', width: 600, height: 500 }) + + it "accepts subsequent same captures after multiple tries", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/fullPage-same') + .screenshot("fullPage-same", { capture: "fullPage" }) + .task("check:screenshot:size", { name: 'fullPage-same.png', width: 600, height: 500 }) + + it "accepts screenshot after multiple tries if somehow app has pixels that match helper pixels", -> + cy + .viewport(1280, 720) + .visit('http://localhost:3322/pathological') + .screenshot("pathological", { capture: "viewport" }) + + it "can capture element screenshots", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/element') + .get(".element") + .screenshot("element") + .task("check:screenshot:size", { name: 'element.png', width: 400, height: 300 }) + + describe "clipping", -> + it "can clip app screenshots", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/color/yellow') + .screenshot("app-clip", { capture: "viewport", clip: { x: 10, y: 10, width: 100, height: 50 }}) + .task("check:screenshot:size", { name: 'app-clip.png', width: 100, height: 50 }) + + it "can clip runner screenshots", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/color/yellow') + .screenshot("runner-clip", { capture: "runner", clip: { x: 15, y: 15, width: 120, height: 60 }}) + .task("check:screenshot:size", { name: 'runner-clip.png', width: 120, height: 60 }) + + it "can clip fullPage screenshots", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/fullPage') + .screenshot("fullPage-clip", { capture: "fullPage", clip: { x: 20, y: 20, width: 140, height: 70 }}) + .task("check:screenshot:size", { name: 'fullPage-clip.png', width: 140, height: 70 }) + + it "can clip element screenshots", -> + cy + .viewport(600, 200) + .visit('http://localhost:3322/element') + .get(".element") + .screenshot("element-clip", { clip: { x: 25, y: 25, width: 160, height: 80 }}) + .task("check:screenshot:size", { name: 'element-clip.png', width: 160, height: 80 }) + context "before hooks", -> before -> ## failure 2 @@ -33,7 +98,7 @@ describe "taking screenshots", -> throw new Error("before each hook failed") afterEach -> - ## failure 4 + ## failure 3 still (since associated only to a single test) throw new Error("after each hook failed") it "empty test 2", -> diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_failing_hook_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_failing_hook_spec.coffee index 0832265d9c77..dce36438873c 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_failing_hook_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_failing_hook_spec.coffee @@ -1,5 +1,25 @@ describe "simple failing hook spec", -> - beforeEach -> - throw new Error('fail') + context "beforeEach hooks", -> + beforeEach -> + throw new Error("fail1") - it "never gets here", -> + it "never gets here", -> + + context "pending", -> + it "is pending" + + context "afterEach hooks", -> + afterEach -> + throw new Error("fail2") + + it "runs this", -> + + it "does not run this", -> + + context "after hooks", -> + after -> + throw new Error("fail3") + + it "runs this", -> + + it "fails on this", -> diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_hooks_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_hooks_spec.coffee new file mode 100644 index 000000000000..0f593a32fa4c --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_hooks_spec.coffee @@ -0,0 +1,21 @@ +describe "simple hooks spec", -> + before -> + cy.wait(100) + + beforeEach -> + cy.wait(200) + + afterEach -> + cy.wait(200) + + after -> + cy.wait(100) + + it "t1", -> + cy.wrap("t1").should("eq", "t1") + + it "t2", -> + cy.wrap("t2").should("eq", "t2") + + it "t3", -> + cy.wrap("t3").should("eq", "t3") diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_passing_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_passing_spec.coffee index 7ad3e35c436e..bb2aa6544677 100644 --- a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_passing_spec.coffee +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_passing_spec.coffee @@ -1,3 +1,6 @@ describe "simple passing spec", -> + beforeEach -> + cy.wait(1000) + it "passes", -> - cy.wrap(true).should("be.true") \ No newline at end of file + cy.wrap(true).should("be.true") diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_spec.coffee new file mode 100644 index 000000000000..346e8e5a9db7 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/simple_spec.coffee @@ -0,0 +1 @@ +it "is true", -> diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/task_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/task_spec.coffee new file mode 100644 index 000000000000..5975a89780ea --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/task_spec.coffee @@ -0,0 +1,5 @@ +it "throws when task returns undefined", -> + cy.task("returns:undefined") + +it "includes stack trace in error", -> + cy.task("errors", "Error thrown in task handler") diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/integration/websockets_spec.coffee b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/websockets_spec.coffee new file mode 100644 index 000000000000..1332717954d0 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/integration/websockets_spec.coffee @@ -0,0 +1,39 @@ +urlClosesWithCode1006 = (win, url) -> + new Promise (resolve, reject) -> + ws = new win.WebSocket(url) + + # ws.onerror = (err) -> + # debugger + + ws.onclose = (evt) -> + if evt.code is 1006 + resolve() + else + reject("websocket connection should have been closed with code 1006 for url: #{url} but was instead closed with code: #{evt.code}") + + ws.onopen = (evt) -> + reject("websocket connection should not have opened for url: #{url}") + +describe "websockets", -> + it "does not crash", -> + cy.visit("http://localhost:3038/foo") + cy.log("should not crash on ECONNRESET websocket upgrade") + cy.window().then (win) -> + Cypress.Promise.all([ + urlClosesWithCode1006(win, "ws://localhost:3038/websocket") + urlClosesWithCode1006(win, "wss://localhost:3040/websocket") + ]) + + cy.log("should be able to send websocket messages") + + cy + .window() + .then (win) -> + new Promise (resolve, reject) -> + ws = new win.WebSocket("ws://localhost:3039/") + ws.onmessage = (evt) -> + resolve(evt.data) + ws.onerror = reject + ws.onopen = -> + ws.send("foo") + .should("eq", "foobar") diff --git a/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js new file mode 100644 index 000000000000..d2f3aa1465e9 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/cypress/plugins/index.js @@ -0,0 +1,60 @@ +const Jimp = require('jimp') +const path = require('path') +const Promise = require('bluebird') + +module.exports = (on) => { + // save some time by only reading the originals once + let cache = {} + function getCachedImage (name) { + const cachedImage = cache[name] + if (cachedImage) return Promise.resolve(cachedImage) + + const imagePath = path.join(__dirname, '..', 'screenshots', `${name}.png`) + return Jimp.read(imagePath).then((image) => { + cache[name] = image + return image + }) + } + + on('task', { + 'returns:undefined' () {}, + + 'errors' (message) { + throw new Error(message) + }, + + 'compare:screenshots' ({ a, b, blackout = false }) { + function isBlack (rgba) { + return `${rgba.r}${rgba.g}${rgba.b}` === '000' + } + + const comparePath = path.join(__dirname, '..', 'screenshots', `${b}.png`) + return Promise.all([ + getCachedImage(a), + Jimp.read(comparePath), + ]) + .spread((originalImage, compareImage) => { + if (blackout && !isBlack(Jimp.intToRGBA(compareImage.getPixelColor(11, 11)))) { + throw new Error('Blackout not present!') + } + + if (originalImage.hash() !== compareImage.hash()) { + throw new Error('Screenshot mismatch!') + } + + return null + }) + }, + + 'check:screenshot:size' ({ name, width, height }) { + return Jimp.read(path.join(__dirname, '..', 'screenshots', name)) + .then((image) => { + if (image.bitmap.width !== width || image.bitmap.height !== height) { + throw new Error(`Screenshot does not match dimensions! Expected: ${width} x ${height} but got ${image.bitmap.width} x ${image.bitmap.height}`) + } + + return null + }) + }, + }) +} diff --git a/packages/server/test/support/fixtures/projects/e2e/reporters/throws.js b/packages/server/test/support/fixtures/projects/e2e/reporters/throws.js new file mode 100644 index 000000000000..cf6bd67a0fb9 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/reporters/throws.js @@ -0,0 +1 @@ +throw new Error('this reporter threw an error') diff --git a/packages/server/test/support/fixtures/projects/e2e/scrollable.html b/packages/server/test/support/fixtures/projects/e2e/scrollable.html new file mode 100644 index 000000000000..4cbf951388f5 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/e2e/scrollable.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> + <title> + + + +
+
top
+
middle
+
bottom
+
+ + diff --git a/packages/server/test/support/fixtures/projects/record/cypress.json b/packages/server/test/support/fixtures/projects/record/cypress.json new file mode 100644 index 000000000000..45fc46124471 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/record/cypress.json @@ -0,0 +1,3 @@ +{ + "projectId": "abc123" +} diff --git a/packages/server/test/support/fixtures/projects/record/cypress/integration/app_spec.js b/packages/server/test/support/fixtures/projects/record/cypress/integration/app_spec.js new file mode 100644 index 000000000000..320e8b21fbbb --- /dev/null +++ b/packages/server/test/support/fixtures/projects/record/cypress/integration/app_spec.js @@ -0,0 +1,5 @@ +/* eslint-disable */ + +it('is true', () => { + expect(true).to.be.true +}) diff --git a/packages/server/test/support/fixtures/projects/task-not-registered/cypress.json b/packages/server/test/support/fixtures/projects/task-not-registered/cypress.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/task-not-registered/cypress.json @@ -0,0 +1 @@ +{} diff --git a/packages/server/test/support/fixtures/projects/task-not-registered/cypress/integration/task_not_registered_spec.coffee b/packages/server/test/support/fixtures/projects/task-not-registered/cypress/integration/task_not_registered_spec.coffee new file mode 100644 index 000000000000..36c4bc6705ce --- /dev/null +++ b/packages/server/test/support/fixtures/projects/task-not-registered/cypress/integration/task_not_registered_spec.coffee @@ -0,0 +1,2 @@ +it "fails because the 'task' event is not registered in plugins file", -> + cy.task("some:task") diff --git a/packages/server/test/support/fixtures/server/expected_stdout_bundle_failures.txt b/packages/server/test/support/fixtures/server/expected_stdout_bundle_failures.txt index 0c8a866aabaf..d66756fc7048 100644 --- a/packages/server/test/support/fixtures/server/expected_stdout_bundle_failures.txt +++ b/packages/server/test/support/fixtures/server/expected_stdout_bundle_failures.txt @@ -1,6 +1,6 @@ -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) Oops...we found an error preparing this test file: /foo/bar/.projects/e2e/cypress/integration/stdout_exit_early_failing_spec.coffee @@ -21,7 +21,7 @@ This occurred while Cypress was compiling and bundling your test code. This is u Fix the error in your code and re-run your tests. - (Tests Finished) + (Results) - Tests: 0 - Passes: 0 @@ -36,7 +36,7 @@ Fix the error in your code and re-run your tests. (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) + (Run Finished) diff --git a/packages/server/test/support/fixtures/server/expected_stdout_failures.txt b/packages/server/test/support/fixtures/server/expected_stdout_failures.txt index d800840bc396..da25cbe97e42 100644 --- a/packages/server/test/support/fixtures/server/expected_stdout_failures.txt +++ b/packages/server/test/support/fixtures/server/expected_stdout_failures.txt @@ -1,6 +1,6 @@ -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) stdout_failing_spec diff --git a/packages/server/test/support/fixtures/server/expected_stdout_failures_outro.txt b/packages/server/test/support/fixtures/server/expected_stdout_failures_outro.txt index 606d65d238ba..6d6476e2c654 100644 --- a/packages/server/test/support/fixtures/server/expected_stdout_failures_outro.txt +++ b/packages/server/test/support/fixtures/server/expected_stdout_failures_outro.txt @@ -1,4 +1,4 @@ - (Tests Finished) + (Results) - Tests: 4 - Passes: 2 @@ -20,7 +20,7 @@ (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) \ No newline at end of file + (Run Finished) \ No newline at end of file diff --git a/packages/server/test/support/fixtures/server/expected_stdout_passing.txt b/packages/server/test/support/fixtures/server/expected_stdout_passing.txt index 15c6b45f5297..c654e0a93729 100644 --- a/packages/server/test/support/fixtures/server/expected_stdout_passing.txt +++ b/packages/server/test/support/fixtures/server/expected_stdout_passing.txt @@ -1,6 +1,6 @@ -Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 +==================================================================================================== - (Tests Starting) + (Run Starting) stdout_passing_spec @@ -21,7 +21,7 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 8 passing - (Tests Finished) + (Results) - Tests: 8 - Passes: 8 @@ -36,7 +36,7 @@ Started video recording: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (Video) - Started processing: Compressing to 32 CRF - - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (0 seconds) + - Finished processing: /foo/bar/.projects/e2e/cypress/videos/abc123.mp4 (X seconds) - (All Done) \ No newline at end of file + (Run Finished) \ No newline at end of file diff --git a/packages/server/test/support/helpers/e2e.coffee b/packages/server/test/support/helpers/e2e.coffee index 1a379f806e3e..1e4d414cc02d 100644 --- a/packages/server/test/support/helpers/e2e.coffee +++ b/packages/server/test/support/helpers/e2e.coffee @@ -1,7 +1,6 @@ require("../../spec_helper") _ = require("lodash") -fs = require("fs-extra") cp = require("child_process") niv = require("npm-install-version") path = require("path") @@ -13,50 +12,71 @@ Promise = require("bluebird") snapshot = require("snap-shot-it") debug = require("debug")("cypress:support:e2e") Fixtures = require("./fixtures") +fs = require("#{root}../lib/util/fs") allowDestroy = require("#{root}../lib/util/server_destroy") user = require("#{root}../lib/user") -stdout = require("#{root}../lib/stdout") +video = require("#{root}../lib/video") cypress = require("#{root}../lib/cypress") Project = require("#{root}../lib/project") +screenshots = require("#{root}../lib/screenshots") settings = require("#{root}../lib/util/settings") cp = Promise.promisifyAll(cp) -fs = Promise.promisifyAll(fs) + +env = _.clone(process.env) Promise.config({ longStackTraces: true }) -env = process.env -env.COPY_CIRCLE_ARTIFACTS = "true" - e2ePath = Fixtures.projectPath("e2e") pathUpToProjectName = Fixtures.projectPath("") stackTraceLinesRe = /(\s+)at\s(.+)/g +browserNameVersionRe = /(Browser\:\s+)(Electron|Chrome|Canary|Chromium)(\s\d+)(\s\(\w+\))?/ +availableBrowsersRe = /(Available browsers found are: )(.+)/g replaceStackTraceLines = (str) -> str.replace(stackTraceLinesRe, "$1at stack trace line") +replaceBrowserName = (str, p1, p2, p3, p4) -> + ## get the padding for the existing browser string + lengthOfExistingBrowserString = _.sum([p2.length, p3.length, _.get(p4, "length", 0)]) + + ## this ensures we add whitespace so the border is not shifted + p1 + _.padEnd("FooBrowser 88", lengthOfExistingBrowserString) + +replaceDurationSeconds = (str, p1, p2, p3, p4) -> + ## get the padding for the existing duration + lengthOfExistingDuration = _.sum([p2.length, p3.length, p4.length]) + + p1 + _.padEnd("X seconds", lengthOfExistingDuration) + +replaceDurationInTables = (str, p1, p2) -> + ## when swapping out the duration, ensure we pad the + ## full length of the duration so it doesn't shift content + p1 + _.padStart("Xs", p2.length) + normalizeStdout = (str) -> ## remove all of the dynamic parts of stdout ## to normalize against what we expected str .split(pathUpToProjectName) .join("/foo/bar/.projects") - .replace(/\(\d{1,2}s\)/g, "(10s)") - .replace(/\s\(\d+m?s\)/g, "") - .replace(/coffee-\d{3}/g, "coffee-456") + .replace(availableBrowsersRe, "$1browser1, browser2, browser3") + .replace(browserNameVersionRe, replaceBrowserName) + .replace(/\s\(\d+m?s\)/g, "") ## numbers in parenths + .replace(/(\s+?)(\d+m?s)/g, replaceDurationInTables) ## durations in tables + .replace(/(coffee|js)-\d{3}/g, "$1-456") .replace(/(.+)(\/.+\.mp4)/g, "$1/abc123.mp4") ## replace dynamic video names - .replace(/Cypress Version\: (.+)/, "Cypress Version: 1.2.3") - .replace(/Duration\: (.+)/, "Duration: 10 seconds") - .replace(/\(\d+ seconds?\)/, "(0 seconds)") + .replace(/(Cypress\:\s+)(\d\.\d\.\d)/g, "$1" + "1.2.3") ## replace Cypress: 2.1.0 + .replace(/(Duration\:\s+)(\d+)(\sseconds?)(\s+)/g, replaceDurationSeconds) + .replace(/\(\d+ seconds?\)/g, "(X seconds)") .replace(/\r/g, "") + .replace("/\(\d{2,4}x\d{2,4}\)/g", "(YYYYxZZZZ)") ## screenshot dimensions .split("\n") .map(replaceStackTraceLines) .join("\n") - .split("2560x1440") ## normalize resolutions - .join("1280x720") startServer = (obj) -> {onServer, port} = obj @@ -76,14 +96,51 @@ startServer = (obj) -> new Promise (resolve) -> srv.listen port, => console.log "listening on port: #{port}" - onServer?(app) + onServer?(app, srv) resolve(srv) stopServer = (srv) -> srv.destroyAsync() +copy = -> + ca = process.env.CIRCLE_ARTIFACTS + + debug("Should copy Circle Artifacts?", Boolean(ca)) + + if ca + videosFolder = path.join(e2ePath, "cypress/videos") + screenshotsFolder = path.join(e2ePath, "cypress/screenshots") + + debug("Copying Circle Artifacts", ca, videosFolder, screenshotsFolder) + + ## copy each of the screenshots and videos + ## to artifacts using each basename of the folders + Promise.join( + screenshots.copy( + screenshotsFolder, + path.join(ca, path.basename(screenshotsFolder)) + ), + video.copy( + videosFolder, + path.join(ca, path.basename(videosFolder)) + ) + ) + module.exports = { + normalizeStdout + + snapshot: (args...) -> + args = _.compact(args) + + ## grab the last element in index + index = args.length - 1 + + ## normalize the stdout of it + args[index] = normalizeStdout(args[index]) + + snapshot.apply(null, args) + setup: (options = {}) -> if npmI = options.npmInstall before -> @@ -124,7 +181,7 @@ module.exports = { Fixtures.scaffold() - @sandbox.stub(process, "exit") + sinon.stub(process, "exit") Promise.try => if servers = options.servers @@ -136,12 +193,12 @@ module.exports = { else @servers = null .then => - if s = options.settings settings.write(e2ePath, s) - .then => afterEach -> + process.env = _.clone(env) + @timeout(human("2 minutes")) Fixtures.remove() @@ -151,6 +208,7 @@ module.exports = { options: (ctx, options = {}) -> _.defaults(options, { + browser: process.env.BROWSER project: e2ePath timeout: if options.exit is false then 3000000 else 120000 }) @@ -179,14 +237,19 @@ module.exports = { if options.headed args.push("--headed") + if options.record + args.push("--record") + + if options.key + args.push("--key=#{options.key}") + if options.reporter args.push("--reporter=#{options.reporter}") if options.reporterOptions args.push("--reporter-options=#{options.reporterOptions}") - ## prefer options if set, else use env - if browser = (options.browser or env.BROWSER) + if browser = (options.browser) args.push("--browser=#{browser}") if options.config @@ -195,6 +258,9 @@ module.exports = { if options.env args.push("--env", options.env) + if options.outputPath + args.push("--output-path", options.outputPath) + if options.exit? args.push("--exit", options.exit) @@ -218,8 +284,54 @@ module.exports = { stdout = "" stderr = "" + exit = (code) -> + if (expected = options.expectedExitCode)? + expect(expected).to.eq(code, "expected exit code") + + ## snapshot the stdout! + if options.snapshot + ## enable callback to modify stdout + if ostd = options.onStdout + stdout = ostd(stdout) + + ## if we have browser in the stdout make + ## sure its legit + if matches = browserNameVersionRe.exec(stdout) + [str, key, browserName, version, headless] = matches + + if b = options.browser + expect(_.capitalize(b)).to.eq(browserName) + + expect(parseFloat(version)).to.be.a.number + + ## if we are in headed mode or in a browser other + ## than electron + if options.headed or (b and b isnt "electron") + expect(headless).not.to.exist + else + expect(headless).to.include("(headless)") + + str = normalizeStdout(stdout) + snapshot(str) + + return { + code: code + stdout: stdout + stderr: stderr + } + new Promise (resolve, reject) -> - sp = cp.spawn "node", args, {env: _.omit(env, "CYPRESS_DEBUG")} + sp = cp.spawn "node", args, { + env: _.chain(process.env) + .omit("CYPRESS_DEBUG") + .extend({ + ## FYI: color will already be disabled + ## because we are piping the child process + COLUMNS: 100 + LINES: 24 + }) + .value() + } ## pipe these to our current process ## so we can see them in the terminal @@ -230,29 +342,19 @@ module.exports = { stdout += buf.toString() sp.stderr.on "data", (buf) -> stderr += buf.toString() - sp.on "error", reject - sp.on "exit", (code) -> - if (expected = options.expectedExitCode)? - try - expect(expected).to.eq(code) - catch err - return reject(err) - - ## snapshot the stdout! - if options.snapshot - try - ## enable callback to modify stdout - if ostd = options.onStdout - stdout = ostd(stdout) - - str = normalizeStdout(stdout) - snapshot(str) - catch err - reject(err) - - resolve({ - code: code - stdout: stdout - stderr: stderr - }) + sp.on("error", reject) + sp.on("exit", resolve) + .tap(copy) + .then(exit) + + sendHtml: (contents) -> (req, res) -> + res.set('Content-Type', 'text/html') + res.send(""" + + + + #{contents} + + + """) } diff --git a/packages/server/test/support/helpers/fixtures.coffee b/packages/server/test/support/helpers/fixtures.coffee index f595036c003b..c9a2fa895bd7 100644 --- a/packages/server/test/support/helpers/fixtures.coffee +++ b/packages/server/test/support/helpers/fixtures.coffee @@ -7,8 +7,6 @@ root = path.join(__dirname, "..", "..", "..") projects = path.join(root, "test", "support", "fixtures", "projects") tmpDir = path.join(root, ".projects") -fs = Promise.promisifyAll(fs) - module.exports = ## copies all of the project fixtures ## to the tmpDir .projects in the root diff --git a/packages/server/test/unit/api_spec.coffee b/packages/server/test/unit/api_spec.coffee index af7115e62e8b..9eb548eaa26e 100644 --- a/packages/server/test/unit/api_spec.coffee +++ b/packages/server/test/unit/api_spec.coffee @@ -11,7 +11,7 @@ Promise = require("bluebird") describe "lib/api", -> beforeEach -> - @sandbox.stub(os, "platform").returns("linux") + sinon.stub(os, "platform").returns("linux") context ".getOrgs", -> it "GET /orgs + returns orgs", -> @@ -19,16 +19,18 @@ describe "lib/api", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/organizations") .reply(200, orgs) api.getOrgs("auth-token-123") .then (ret) -> - expect(ret).to.eql(orgs) + expect(ret).to.deep.eq(orgs) it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/organizations") .reply(500, {}) @@ -44,16 +46,18 @@ describe "lib/api", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects") .reply(200, projects) api.getProjects("auth-token-123") .then (ret) -> - expect(ret).to.eql(projects) + expect(ret).to.deep.eq(projects) it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects") .reply(500, {}) @@ -69,17 +73,19 @@ describe "lib/api", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .matchHeader("x-route-version", "2") .get("/projects/id-123") .reply(200, project) api.getProject("id-123", "auth-token-123") .then (ret) -> - expect(ret).to.eql(project) + expect(ret).to.deep.eq(project) it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects/id-123") .reply(500, {}) @@ -90,22 +96,25 @@ describe "lib/api", -> expect(err.isApiError).to.be.true context ".getProjectRuns", -> - it "GET /projects/:id/builds + returns builds", -> - builds = [] + it "GET /projects/:id/runs + returns runs", -> + runs = [] nock("http://localhost:1234") + .matchHeader("x-route-version", "2") .matchHeader("authorization", "Bearer auth-token-123") - .get("/projects/id-123/builds") - .reply(200, builds) + .matchHeader("accept-encoding", /gzip/) + .get("/projects/id-123/runs") + .reply(200, runs) api.getProjectRuns("id-123", "auth-token-123") .then (ret) -> - expect(ret).to.eql(builds) + expect(ret).to.deep.eq(runs) it "handles timeouts", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") - .get("/projects/id-123/builds") + .matchHeader("accept-encoding", /gzip/) + .get("/projects/id-123/runs") .socketDelay(5000) .reply(200, []) @@ -116,7 +125,7 @@ describe "lib/api", -> expect(err.message).to.eq("Error: ESOCKETTIMEDOUT") it "sets timeout to 10 seconds", -> - @sandbox.stub(rp, "get").returns({ + sinon.stub(rp, "get").returns({ catch: -> { catch: -> { then: (fn) -> fn() @@ -130,10 +139,11 @@ describe "lib/api", -> .then (ret) -> expect(rp.get).to.be.calledWithMatch({timeout: 10000}) - it "GET /projects/:id/builds failure formatting", -> + it "GET /projects/:id/runs failure formatting", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") - .get("/projects/id-123/builds") + .matchHeader("accept-encoding", /gzip/) + .get("/projects/id-123/runs") .reply(401, { errors: { permission: ["denied"] @@ -159,7 +169,8 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") - .get("/projects/id-123/builds") + .matchHeader("accept-encoding", /gzip/) + .get("/projects/id-123/runs") .reply(500, {}) api.getProjectRuns("id-123", "auth-token-123") @@ -171,7 +182,7 @@ describe "lib/api", -> context ".ping", -> it "GET /ping", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .get("/ping") .reply(200, "OK") @@ -183,6 +194,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/ping") .reply(500, {}) @@ -197,69 +209,50 @@ describe "lib/api", -> @buildProps = { projectId: "id-123" recordKey: "token-123" - commitSha: "sha" - commitBranch: "master" - commitAuthorName: "brian" - commitAuthorEmail: "brian@cypress.io" - commitMessage: "such hax" - remoteOrigin: "https://github.com/foo/bar.git" - ciProvider: "circle" - ciBuildNumber: "987" - ciParams: { foo: "bar" } + ci: { + provider: "circle" + buildNumber: "987" + params: { foo: "bar" } + } + platform: {} + commit: { + sha: "sha" + branch: "master" + authorName: "brian" + authorEmail: "brian@cypress.io" + message: "such hax" + remoteOrigin: "https://github.com/foo/bar.git" + } specs: ["foo.js", "bar.js"] } - it "POST /builds + returns buildId", -> + it "POST /runs + returns runId", -> nock("http://localhost:1234") - .matchHeader("x-route-version", "2") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "3") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) - .post("/builds", @buildProps) + .post("/runs", @buildProps) .reply(200, { - buildId: "new-build-id-123" + runId: "new-run-id-123" }) api.createRun(@buildProps) .then (ret) -> - expect(ret).to.eq("new-build-id-123") + expect(ret).to.deep.eq({ runId: "new-run-id-123" }) - it "POST /builds failure formatting", -> + it "POST /runs failure formatting", -> nock("http://localhost:1234") - .matchHeader("x-route-version", "2") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "3") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) - .post("/builds", { - projectId: null - recordKey: "token-123" - commitSha: "sha" - commitBranch: "master" - commitAuthorName: "brian" - commitAuthorEmail: "brian@cypress.io" - commitMessage: "such hax" - remoteOrigin: "https://github.com/foo/bar.git" - ciProvider: "circle" - ciBuildNumber: "987" - ciParams: { foo: "bar" } - }) + .post("/runs", @buildProps) .reply(422, { errors: { - buildId: ["is required"] + runId: ["is required"] } }) - api.createRun({ - projectId: null - recordKey: "token-123" - commitSha: "sha" - commitBranch: "master" - commitAuthorName: "brian" - commitAuthorEmail: "brian@cypress.io" - commitMessage: "such hax" - remoteOrigin: "https://github.com/foo/bar.git" - ciProvider: "circle" - ciBuildNumber: "987" - ciParams: { foo: "bar" } - }) + api.createRun(@buildProps) .then -> throw new Error("should have thrown here") .catch (err) -> @@ -268,7 +261,7 @@ describe "lib/api", -> { "errors": { - "buildId": [ + "runId": [ "is required" ] } @@ -277,10 +270,10 @@ describe "lib/api", -> it "handles timeouts", -> nock("http://localhost:1234") - .matchHeader("x-route-version", "2") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "3") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) - .post("/builds") + .post("/runs") .socketDelay(5000) .reply(200, {}) @@ -293,9 +286,7 @@ describe "lib/api", -> expect(err.message).to.eq("Error: ESOCKETTIMEDOUT") it "sets timeout to 10 seconds", -> - @sandbox.stub(rp, "post").returns({ - promise: () -> Promise.resolve({buildId: 'foo'}) - }) + sinon.stub(rp, "post").resolves({runId: 'foo'}) api.createRun({}) .then -> @@ -303,8 +294,10 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") + .matchHeader("x-route-version", "3") .matchHeader("authorization", "Bearer auth-token-123") - .post("/builds", @buildProps) + .matchHeader("accept-encoding", /gzip/) + .post("/runs", @buildProps) .reply(500, {}) api.createRun(@buildProps) @@ -319,64 +312,45 @@ describe "lib/api", -> value: "53" }) - @postProps = { - spec: "cypress/integration/app_spec.js" - browserName: "Foo" - browserVersion: "1.2.3" - osName: "darwin" - osVersion: "10.10.10" - osCpus: [{model: "foo"}] - osMemory: { - free: 1000 - total: 2000 - } - } - @createProps = { - buildId: "build-id-123" - browser: "foo" + runId: "run-id-123" spec: "cypress/integration/app_spec.js" + planId: "planId123" + machineId: "machineId123" + platform: {} } - it "POSTs /builds/:id/instances", -> - @sandbox.stub(os, "release").returns("10.10.10") - @sandbox.stub(os, "cpus").returns([{model: "foo"}]) - @sandbox.stub(os, "freemem").returns(1000) - @sandbox.stub(os, "totalmem").returns(2000) - @sandbox.stub(browsers, "getByName").resolves({ - displayName: "Foo" - version: "1.2.3" - }) + @postProps = _.omit(@createProps, "runId") + it "POSTs /runs/:id/instances", -> os.platform.returns("darwin") nock("http://localhost:1234") - .matchHeader("x-route-version", "3") - .matchHeader("x-platform", "darwin") + .matchHeader("x-route-version", "4") + .matchHeader("x-os-name", "darwin") .matchHeader("x-cypress-version", pkg.version) - .post("/builds/build-id-123/instances", @postProps) + .post("/runs/run-id-123/instances", @postProps) .reply(200, { instanceId: "instance-id-123" }) api.createInstance(@createProps) .then (instanceId) -> - expect(browsers.getByName).to.be.calledWith("foo") expect(instanceId).to.eq("instance-id-123") - it "POST /builds/:id/instances failure formatting", -> + it "POST /runs/:id/instances failure formatting", -> nock("http://localhost:1234") - .matchHeader("x-route-version", "3") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "4") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) - .post("/builds/build-id-123/instances") + .post("/runs/run-id-123/instances") .reply(422, { errors: { tests: ["is required"] } }) - api.createInstance({buildId: "build-id-123"}) + api.createInstance({runId: "run-id-123"}) .then -> throw new Error("should have thrown here") .catch (err) -> @@ -394,15 +368,15 @@ describe "lib/api", -> it "handles timeouts", -> nock("http://localhost:1234") - .matchHeader("x-route-version", "3") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "4") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) - .post("/builds/build-id-123/instances") + .post("/runs/run-id-123/instances") .socketDelay(5000) .reply(200, {}) api.createInstance({ - buildId: "build-id-123" + runId: "run-id-123" timeout: 100 }) .then -> @@ -411,17 +385,8 @@ describe "lib/api", -> expect(err.message).to.eq("Error: ESOCKETTIMEDOUT") it "sets timeout to 10 seconds", -> - @sandbox.stub(rp, "post").returns({ - promise: -> { - get: -> { - catch: -> { - catch: -> { - then: (fn) -> fn() - } - then: (fn) -> fn() - } - } - } + sinon.stub(rp, "post").returns({ + promise: -> Promise.resolve({ instanceId: "instanceId123" }) }) api.createInstance({}) @@ -431,7 +396,8 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") - .post("/builds/build-id-123/instances", @postProps) + .matchHeader("accept-encoding", /gzip/) + .post("/runs/run-id-123/instances", @postProps) .reply(500, {}) api.createInstance(@createProps) @@ -442,44 +408,23 @@ describe "lib/api", -> context ".updateInstance", -> beforeEach -> - Object.defineProperty(process.versions, "chrome", { - value: "53" - }) - - @putProps = { - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - error: "err msg" - video: true - screenshots: [] - failingTests: [] - cypressConfig: {} - ciProvider: "circle" - stdout: "foo\nbar\nbaz" - } - @updateProps = { instanceId: "instance-id-123" - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 + stats: {} error: "err msg" video: true screenshots: [] - failingTests: [] cypressConfig: {} - ciProvider: "circle" + reporterStats: {} stdout: "foo\nbar\nbaz" } + @putProps = _.omit(@updateProps, "instanceId") + it "PUTs /instances/:id", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "2") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .put("/instances/instance-id-123", @putProps) .reply(200) @@ -488,7 +433,8 @@ describe "lib/api", -> it "PUT /instances/:id failure formatting", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "2") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .put("/instances/instance-id-123") .reply(422, { @@ -515,7 +461,8 @@ describe "lib/api", -> it "handles timeouts", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-route-version", "2") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .put("/instances/instance-id-123") .socketDelay(5000) @@ -531,7 +478,7 @@ describe "lib/api", -> expect(err.message).to.eq("Error: ESOCKETTIMEDOUT") it "sets timeout to 10 seconds", -> - @sandbox.stub(rp, "put").resolves() + sinon.stub(rp, "put").resolves() api.updateInstance({}) .then -> @@ -539,7 +486,9 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") + .matchHeader("x-route-version", "2") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .put("/instances/instance-id-123", @putProps) .reply(500, {}) @@ -552,7 +501,7 @@ describe "lib/api", -> context ".updateInstanceStdout", -> it "PUTs /instances/:id/stdout", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .put("/instances/instance-id-123/stdout", { stdout: "foobarbaz\n" @@ -566,7 +515,7 @@ describe "lib/api", -> it "PUT /instances/:id/stdout failure formatting", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .put("/instances/instance-id-123/stdout") .reply(422, { @@ -593,7 +542,7 @@ describe "lib/api", -> it "handles timeouts", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .put("/instances/instance-id-123/stdout") .socketDelay(5000) @@ -609,7 +558,7 @@ describe "lib/api", -> expect(err.message).to.eq("Error: ESOCKETTIMEDOUT") it "sets timeout to 10 seconds", -> - @sandbox.stub(rp, "put").resolves() + sinon.stub(rp, "put").resolves() api.updateInstanceStdout({}) .then -> @@ -618,6 +567,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .put("/instances/instance-id-123/stdout", { stdout: "foobarbaz\n" }) @@ -635,7 +585,7 @@ describe "lib/api", -> context ".getLoginUrl", -> it "GET /auth + returns the url", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .get("/auth") .reply(200, { @@ -648,6 +598,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/auth") .reply(500, {}) @@ -659,10 +610,10 @@ describe "lib/api", -> context ".createSignin", -> it "POSTs /signin + returns user object", -> - @sandbox.stub(nmi, "machineId").resolves("12345") + sinon.stub(nmi, "machineId").resolves("12345") nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("x-route-version", "3") .matchHeader("x-machine-id", "12345") @@ -678,12 +629,12 @@ describe "lib/api", -> }) it "handles nmi errors", -> - @sandbox.stub(nmi, "machineId").rejects(new Error("foo")) + sinon.stub(nmi, "machineId").rejects(new Error("foo")) nock("http://localhost:1234", { "badheaders": ["x-machine-id"] }) - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("x-route-version", "3") .matchHeader("x-accept-terms", "true") @@ -700,7 +651,7 @@ describe "lib/api", -> it "handles 401 exceptions", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("x-route-version", "3") .post("/signin") @@ -716,6 +667,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/signin") .reply(500, {}) @@ -728,9 +680,10 @@ describe "lib/api", -> context ".createSignout", -> it "POSTs /signout", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/signout") .reply(200) @@ -739,6 +692,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/signout") .reply(500, {}) @@ -765,10 +719,11 @@ describe "lib/api", -> it "POST /projects", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("x-route-version", "2") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/projects", @postProps) .reply(200, { id: "id-123" @@ -779,7 +734,7 @@ describe "lib/api", -> api.createProject(@createProps, "remoteOrigin", "auth-token-123") .then (projectDetails) -> - expect(projectDetails).to.eql({ + expect(projectDetails).to.deep.eq({ id: "id-123" name: "foobar" orgId: "org-id-123" @@ -788,10 +743,11 @@ describe "lib/api", -> it "POST /projects failure formatting", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("x-route-version", "2") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/projects", { name: "foobar" orgId: "org-id-123" @@ -829,6 +785,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/projects", @postProps) .reply(500, {}) @@ -844,16 +801,18 @@ describe "lib/api", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects/id-123/keys") .reply(200, recordKeys) api.getProjectRecordKeys("id-123", "auth-token-123") .then (ret) -> - expect(ret).to.eql(recordKeys) + expect(ret).to.deep.eq(recordKeys) it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects/id-123/keys") .reply(500, {}) @@ -867,6 +826,7 @@ describe "lib/api", -> it "POST /projects/:id/membership_requests + returns response", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/projects/project-id-123/membership_requests") .reply(200) @@ -877,6 +837,7 @@ describe "lib/api", -> it "POST /projects/:id/membership_requests failure formatting", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/projects/project-id-123/membership_requests") .reply(422, { errors: { @@ -903,6 +864,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/projects/project-id-123/membership_requests") .reply(500, {}) @@ -915,9 +877,10 @@ describe "lib/api", -> context ".getProjectToken", -> it "GETs /projects/:id/token", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects/project-123/token") .reply(200, { apiToken: "token-123" @@ -930,6 +893,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .get("/projects/project-123/token") .reply(500, {}) @@ -942,9 +906,10 @@ describe "lib/api", -> context ".updateProjectToken", -> it "PUTs /projects/:id/token", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .put("/projects/project-123/token") .reply(200, { apiToken: "token-123" @@ -957,6 +922,7 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .put("/projects/project-id-123/token") .reply(500, {}) @@ -970,7 +936,7 @@ describe "lib/api", -> beforeEach -> @setup = (body, authToken, delay = 0) -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("authorization", "Bearer #{authToken}") .post("/exceptions", body) @@ -985,8 +951,8 @@ describe "lib/api", -> ## return our own specific promise ## so we can spy on the timeout function p = Promise.resolve() - @sandbox.spy(p, "timeout") - @sandbox.stub(rp.Request.prototype, "promise").returns(p) + sinon.spy(p, "timeout") + sinon.stub(rp.Request.prototype, "promise").returns(p) @setup({foo: "bar"}, "auth-token-123") api.createRaygunException({foo: "bar"}, "auth-token-123").then -> @@ -1004,9 +970,10 @@ describe "lib/api", -> it "tags errors", -> nock("http://localhost:1234") - .matchHeader("x-platform", "linux") + .matchHeader("x-os-name", "linux") .matchHeader("x-cypress-version", pkg.version) .matchHeader("authorization", "Bearer auth-token-123") + .matchHeader("accept-encoding", /gzip/) .post("/exceptions", {foo: "bar"}) .reply(500, {}) diff --git a/packages/server/test/unit/args_spec.coffee b/packages/server/test/unit/args_spec.coffee index ddf5c011002b..ba3d8ea26c2e 100644 --- a/packages/server/test/unit/args_spec.coffee +++ b/packages/server/test/unit/args_spec.coffee @@ -16,16 +16,16 @@ describe "lib/util/args", -> expect(options.pong).to.eq 123 context "--project", -> - it "sets projectPath", -> - projectPath = path.resolve(cwd, "./foo/bar") + it "sets projectRoot", -> + projectRoot = path.resolve(cwd, "./foo/bar") options = @setup("--project", "./foo/bar") - expect(options.projectPath).to.eq projectPath + expect(options.projectRoot).to.eq projectRoot context "--run-project", -> - it "sets projectPath", -> - projectPath = path.resolve(cwd, "/baz") + it "sets projectRoot", -> + projectRoot = path.resolve(cwd, "/baz") options = @setup("--run-project", "/baz") - expect(options.projectPath).to.eq projectPath + expect(options.projectRoot).to.eq projectRoot it "strips single double quote from the end", -> # https://github.com/cypress-io/cypress/issues/535 diff --git a/packages/server/test/unit/browsers/browsers_spec.coffee b/packages/server/test/unit/browsers/browsers_spec.coffee index f83b58552833..888132e90c54 100644 --- a/packages/server/test/unit/browsers/browsers_spec.coffee +++ b/packages/server/test/unit/browsers/browsers_spec.coffee @@ -6,25 +6,33 @@ browsers = require("#{root}../lib/browsers") utils = require("#{root}../lib/browsers/utils") describe "lib/browsers/index", -> - context ".getByName", -> + context ".ensureAndGetByName", -> it "returns browser by name", -> - @sandbox.stub(utils, "getBrowsers").resolves([ + sinon.stub(utils, "getBrowsers").resolves([ { name: "foo" } { name: "bar" } ]) - browsers.getByName("foo").then (browser) -> + browsers.ensureAndGetByName("foo") + .then (browser) -> expect(browser).to.deep.eq({ name: "foo" }) + it "throws when no browser can be found", -> + browsers.ensureAndGetByName("browserNotGonnaBeFound") + .then -> + throw new Error("should have failed") + .catch (err) -> + expect(err.type).to.eq("BROWSER_NOT_FOUND") + context ".open", -> # it "calls onBrowserClose callback on close", -> - # onBrowserClose = @sandbox.stub() + # onBrowserClose = sinon.stub() # browsers.launch("electron", @url, {onBrowserClose}).then -> # Windows.create.lastCall.args[0].onClose() # expect(onBrowserClose).to.be.called # # it "calls onBrowserOpen callback", -> - # onBrowserOpen = @sandbox.stub() + # onBrowserOpen = sinon.stub() # browsers.launch("electron", @url, {onBrowserOpen}).then => # expect(onBrowserOpen).to.be.called # diff --git a/packages/server/test/unit/browsers/chrome_spec.coffee b/packages/server/test/unit/browsers/chrome_spec.coffee index e3966b5037f4..27a3fb0e7017 100644 --- a/packages/server/test/unit/browsers/chrome_spec.coffee +++ b/packages/server/test/unit/browsers/chrome_spec.coffee @@ -12,13 +12,13 @@ describe "lib/browsers/chrome", -> beforeEach -> @args = [] - @sandbox.stub(chrome, "_getArgs").returns(@args) - @sandbox.stub(chrome, "_writeExtension").resolves("/path/to/ext") - @sandbox.stub(plugins, "has") - @sandbox.stub(plugins, "execute") - @sandbox.stub(utils, "launch") - @sandbox.stub(utils, "getProfileDir").returns("/profile/dir") - @sandbox.stub(utils, "ensureCleanCache").resolves("/profile/dir/CypressCache") + sinon.stub(chrome, "_getArgs").returns(@args) + sinon.stub(chrome, "_writeExtension").resolves("/path/to/ext") + sinon.stub(plugins, "has") + sinon.stub(plugins, "execute") + sinon.stub(utils, "launch") + sinon.stub(utils, "getProfileDir").returns("/profile/dir") + sinon.stub(utils, "ensureCleanCache").resolves("/profile/dir/CypressCache") it "is noop without before:browser:launch", -> plugins.has.returns(false) @@ -79,28 +79,28 @@ describe "lib/browsers/chrome", -> context "#_getArgs", -> it "disables gpu when linux", -> - @sandbox.stub(os, "platform").returns("linux") + sinon.stub(os, "platform").returns("linux") args = chrome._getArgs() expect(args).to.include("--disable-gpu") it "does not disable gpu when not linux", -> - @sandbox.stub(os, "platform").returns("darwin") + sinon.stub(os, "platform").returns("darwin") args = chrome._getArgs() expect(args).not.to.include("--disable-gpu") it "turns off sandbox when linux", -> - @sandbox.stub(os, "platform").returns("linux") + sinon.stub(os, "platform").returns("linux") args = chrome._getArgs() expect(args).to.include("--no-sandbox") it "does not turn off sandbox when not linux", -> - @sandbox.stub(os, "platform").returns("win32") + sinon.stub(os, "platform").returns("win32") args = chrome._getArgs() diff --git a/packages/server/test/unit/browsers/electron_spec.coffee b/packages/server/test/unit/browsers/electron_spec.coffee index f21c5abd9e47..473ce7d0d5d2 100644 --- a/packages/server/test/unit/browsers/electron_spec.coffee +++ b/packages/server/test/unit/browsers/electron_spec.coffee @@ -17,18 +17,18 @@ describe "lib/browsers/electron", -> @state = {} @options = { some: "var" - projectPath: "/foo/" + projectRoot: "/foo/" } @automation = Automation.create("foo", "bar", "baz") @win = _.extend(new EE(), { - close: @sandbox.stub() - loadURL: @sandbox.stub() + close: sinon.stub() + loadURL: sinon.stub() webContents: { session: { cookies: { - get: @sandbox.stub() - set: @sandbox.stub() - remove: @sandbox.stub() + get: sinon.stub() + set: sinon.stub() + remove: sinon.stub() } } } @@ -36,16 +36,16 @@ describe "lib/browsers/electron", -> context ".open", -> beforeEach -> - @sandbox.stub(electron, "_render").resolves(@win) + sinon.stub(electron, "_render").resolves(@win) savedState() .then (state) => la(check.fn(state.get), "state is missing .get to stub", state) - @sandbox.stub(state, "get").resolves(@state) + sinon.stub(state, "get").resolves(@state) it "calls render with url, state, and options", -> electron.open("electron", @url, @options, @automation) .then => - options = electron._defaultOptions(@options.projectPath, @state, @options) + options = electron._defaultOptions(@options.projectRoot, @state, @options) options = Windows.defaults(options) @@ -55,7 +55,7 @@ describe "lib/browsers/electron", -> expect(electron._render).to.be.calledWith( @url, - @options.projectPath, + @options.projectRoot, ) it "returns custom object emitter interface", -> @@ -67,10 +67,10 @@ describe "lib/browsers/electron", -> context "._launch", -> beforeEach -> - @sandbox.stub(menu, "set") - @sandbox.stub(electron, "_clearCache").resolves() - @sandbox.stub(electron, "_setProxy").resolves() - @sandbox.stub(electron, "_setUserAgent") + sinon.stub(menu, "set") + sinon.stub(electron, "_clearCache").resolves() + sinon.stub(electron, "_setProxy").resolves() + sinon.stub(electron, "_setUserAgent") it "sets dev tools in menu", -> electron._launch(@win, @url, @options) @@ -109,22 +109,22 @@ describe "lib/browsers/electron", -> beforeEach -> @newWin = {} - @sandbox.stub(menu, "set") - @sandbox.stub(electron, "_setProxy").resolves() - @sandbox.stub(electron, "_launch").resolves() - @sandbox.stub(Windows, "create") - .withArgs(@options.projectPath, @options) + sinon.stub(menu, "set") + sinon.stub(electron, "_setProxy").resolves() + sinon.stub(electron, "_launch").resolves() + sinon.stub(Windows, "create") + .withArgs(@options.projectRoot, @options) .returns(@newWin) it "creates window instance and calls launch with window", -> - electron._render(@url, @options.projectPath, @options) + electron._render(@url, @options.projectRoot, @options) .then => - expect(Windows.create).to.be.calledWith(@options.projectPath, @options) + expect(Windows.create).to.be.calledWith(@options.projectRoot, @options) expect(electron._launch).to.be.calledWith(@newWin, @url, @options) context "._defaultOptions", -> beforeEach -> - @sandbox.stub(menu, "set") + sinon.stub(menu, "set") it "uses default width if there isn't one saved", -> opts = electron._defaultOptions("/foo", @state, @options) @@ -170,34 +170,34 @@ describe "lib/browsers/electron", -> describe ".onNewWindow", -> beforeEach -> - @sandbox.stub(electron, "_launchChild").resolves(@win) + sinon.stub(electron, "_launchChild").resolves(@win) it "passes along event, url, parent window and options", -> - opts = electron._defaultOptions(@options.projectPath, @state, @options) + opts = electron._defaultOptions(@options.projectRoot, @state, @options) event = {} parentWindow = { - on: @sandbox.stub() + on: sinon.stub() } opts.onNewWindow.call(parentWindow, event, @url) expect(electron._launchChild).to.be.calledWith( - event, @url, parentWindow, @options.projectPath, @state, @options + event, @url, parentWindow, @options.projectRoot, @state, @options ) ## TODO: these all need to be updated context.skip "._launchChild", -> beforeEach -> @childWin = _.extend(new EE(), { - close: @sandbox.stub() - isDestroyed: @sandbox.stub().returns(false) + close: sinon.stub() + isDestroyed: sinon.stub().returns(false) webContents: new EE() }) Windows.create.onCall(1).resolves(@childWin) - @event = {preventDefault: @sandbox.stub()} + @event = {preventDefault: sinon.stub()} @win.getPosition = -> [4, 2] @openNewWindow = (options) => @@ -275,8 +275,8 @@ describe "lib/browsers/electron", -> it "does the same things for children of the child window", -> @grandchildWin = _.extend(new EE(), { - close: @sandbox.stub() - isDestroyed: @sandbox.stub().returns(false) + close: sinon.stub() + isDestroyed: sinon.stub().returns(false) webContents: new EE() }) Windows.create.onCall(2).resolves(@grandchildWin) @@ -295,7 +295,7 @@ describe "lib/browsers/electron", -> it "sets proxy rules for webContents", -> webContents = { session: { - setProxy: @sandbox.stub().yieldsAsync() + setProxy: sinon.stub().yieldsAsync() } } diff --git a/packages/server/test/unit/cache_spec.coffee b/packages/server/test/unit/cache_spec.coffee index 666e9fba0394..58d6930ff699 100644 --- a/packages/server/test/unit/cache_spec.coffee +++ b/packages/server/test/unit/cache_spec.coffee @@ -1,14 +1,12 @@ require("../spec_helper") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") cwd = require("#{root}lib/cwd") cache = require("#{root}lib/cache") +fs = require("#{root}lib/util/fs") Fixtures = require("../support/helpers/fixtures") -fs = Promise.promisifyAll(fs) - describe "lib/cache", -> beforeEach -> cache.remove() @@ -104,9 +102,9 @@ describe "lib/cache", -> cache.__get("PROJECTS").then (projects) -> expect(projects).to.deep.eq [] - describe "#getProjectPaths", -> + describe "#getProjectRoots", -> beforeEach -> - @statAsync = @sandbox.stub(fs, "statAsync") + @statAsync = sinon.stub(fs, "statAsync") it "returns an array of paths", -> @statAsync.withArgs("/Users/brian/app").resolves() @@ -116,7 +114,7 @@ describe "lib/cache", -> .then => cache.insertProject("/Users/sam/app2") .then => - cache.getProjectPaths().then (paths) -> + cache.getProjectRoots().then (paths) -> expect(paths).to.deep.eq ["/Users/sam/app2", "/Users/brian/app"] it "removes any paths which no longer exist on the filesystem", -> @@ -127,7 +125,7 @@ describe "lib/cache", -> .then => cache.insertProject("/Users/sam/app2") .then => - cache.getProjectPaths().then (paths) => + cache.getProjectRoots().then (paths) => expect(paths).to.deep.eq ["/Users/brian/app"] .then => ## we have to wait on the write event because diff --git a/packages/server/test/unit/stdout_spec.coffee b/packages/server/test/unit/capture_spec.coffee similarity index 80% rename from packages/server/test/unit/stdout_spec.coffee rename to packages/server/test/unit/capture_spec.coffee index 6ea2c9613a14..c789e0ee1bf8 100644 --- a/packages/server/test/unit/stdout_spec.coffee +++ b/packages/server/test/unit/capture_spec.coffee @@ -1,15 +1,15 @@ require("../spec_helper") -stdout = require("#{root}lib/stdout") +capture = require("#{root}lib/capture") -describe "lib/stdout", -> +describe "lib/capture", -> afterEach -> - stdout.restore() + capture.restore() context "process.stdout.write", -> beforeEach -> - @write = @sandbox.spy(process.stdout, "write") - @captured = stdout.capture() + @write = sinon.spy(process.stdout, "write") + @captured = capture.stdout() it "slurps up stdout", -> console.log("foo") @@ -32,9 +32,9 @@ describe "lib/stdout", -> context "process.log", -> beforeEach -> @log = process.log - @logStub = process.log = @sandbox.stub() + @logStub = process.log = sinon.stub() - @captured = stdout.capture() + @captured = capture.stdout() afterEach -> process.log = @log diff --git a/packages/server/test/unit/ci_provider_spec.coffee b/packages/server/test/unit/ci_provider_spec.coffee index db7b3dc211d3..2a548175fad1 100644 --- a/packages/server/test/unit/ci_provider_spec.coffee +++ b/packages/server/test/unit/ci_provider_spec.coffee @@ -17,7 +17,7 @@ describe "lib/util/ci_provider", -> ## restore the env process.env = JSON.parse(@env) - context "grou-id", -> + context "group-id", -> describe "circle", -> beforeEach -> process.env.CIRCLECI = true @@ -105,11 +105,15 @@ describe "lib/util/ci_provider", -> it "drone", -> process.env.DRONE = true + process.env.DRONE_BUILD_NUMBER = "1234" + process.env.DRONE_BUILD_LINK = "some url" @expects({ name: "drone", - buildNum: null - params: null + buildNum: "1234" + params: { + buildUrl: "some url" + } }) it "gitlab via GITLAB_CI", -> diff --git a/packages/server/test/unit/config_spec.coffee b/packages/server/test/unit/config_spec.coffee index 885582e2712a..cc5a7e03456f 100644 --- a/packages/server/test/unit/config_spec.coffee +++ b/packages/server/test/unit/config_spec.coffee @@ -19,21 +19,21 @@ describe "lib/config", -> context ".get", -> beforeEach -> - @projectPath = "/_test-output/path/to/project" + @projectRoot = "/_test-output/path/to/project" @setup = (cypressJson = {}, cypressEnvJson = {}) => - @sandbox.stub(settings, "read").withArgs(@projectPath).resolves(cypressJson) - @sandbox.stub(settings, "readEnv").withArgs(@projectPath).resolves(cypressEnvJson) + sinon.stub(settings, "read").withArgs(@projectRoot).resolves(cypressJson) + sinon.stub(settings, "readEnv").withArgs(@projectRoot).resolves(cypressEnvJson) it "sets projectRoot", -> @setup({}, {foo: "bar"}) - config.get(@projectPath) + config.get(@projectRoot) .then (obj) => - expect(obj.projectRoot).to.eq(@projectPath) + expect(obj.projectRoot).to.eq(@projectRoot) expect(obj.env).to.deep.eq({foo: "bar"}) it "sets projectName", -> @setup({}, {foo: "bar"}) - config.get(@projectPath) + config.get(@projectRoot) .then (obj) -> expect(obj.projectName).to.eq("project") @@ -42,27 +42,27 @@ describe "lib/config", -> @setup({}, {foo: "bar"}) it "can override default port", -> - config.get(@projectPath, {port: 8080}) + config.get(@projectRoot, {port: 8080}) .then (obj) -> expect(obj.port).to.eq(8080) it "updates browserUrl", -> - config.get(@projectPath, {port: 8080}) + config.get(@projectRoot, {port: 8080}) .then (obj) -> expect(obj.browserUrl).to.eq "http://localhost:8080/__/" it "updates proxyUrl", -> - config.get(@projectPath, {port: 8080}) + config.get(@projectRoot, {port: 8080}) .then (obj) -> expect(obj.proxyUrl).to.eq "http://localhost:8080" context "validation", -> beforeEach -> @expectValidationPasses = => - config.get(@projectPath) ## shouldn't throw + config.get(@projectRoot) ## shouldn't throw @expectValidationFails = (errorMessage = "validation error") => - config.get(@projectPath) + config.get(@projectRoot) .then -> throw new Error("should throw validation error") .catch (err) -> @@ -159,6 +159,15 @@ describe "lib/config", -> @setup({execTimeout: "foo"}) @expectValidationFails("be a number") + context "taskTimeout", -> + it "passes if a number", -> + @setup({taskTimeout: 10}) + @expectValidationPasses() + + it "fails if not a number", -> + @setup({taskTimeout: "foo"}) + @expectValidationFails("be a number") + context "fileServerFolder", -> it "passes if a string", -> @setup({fileServerFolder: "_files"}) @@ -285,15 +294,6 @@ describe "lib/config", -> @setup({responseTimeout: "foo"}) @expectValidationFails("be a number") - context "screenshotOnHeadlessFailure", -> - it "passes if a boolean", -> - @setup({screenshotOnHeadlessFailure: false}) - @expectValidationPasses() - - it "fails if not a boolean", -> - @setup({screenshotOnHeadlessFailure: 42}) - @expectValidationFails("be a boolean") - context "testFiles", -> it "passes if a string", -> @setup({testFiles: "**/*.coffee"}) @@ -580,9 +580,6 @@ describe "lib/config", -> it "numTestsKeptInMemory=50", -> @defaults "numTestsKeptInMemory", 50 - it "screenshotOnHeadlessFailure=true", -> - @defaults "screenshotOnHeadlessFailure", true - it "modifyObstructiveCode=true", -> @defaults "modifyObstructiveCode", true @@ -624,12 +621,12 @@ describe "lib/config", -> hosts: "foo=bar|baz=quux" }) - it "resets numTestsKeptInMemory to 0 when headless", -> + it "resets numTestsKeptInMemory to 0 when runMode", -> config.mergeDefaults({projectRoot: "/foo/bar/"}, {isTextTerminal: true}) .then (cfg) -> expect(cfg.numTestsKeptInMemory).to.eq(0) - it "resets watchForFileChanges to false when headless", -> + it "resets watchForFileChanges to false when runMode", -> config.mergeDefaults({projectRoot: "/foo/bar/"}, {isTextTerminal: true}) .then (cfg) -> expect(cfg.watchForFileChanges).to.be.false @@ -725,7 +722,7 @@ describe "lib/config", -> requestTimeout: { value: 5000, from: "default" }, responseTimeout: { value: 30000, from: "default" }, execTimeout: { value: 60000, from: "default" }, - screenshotOnHeadlessFailure:{ value: true, from: "default" }, + taskTimeout: { value: 60000, from: "default" }, numTestsKeptInMemory: { value: 50, from: "default" }, waitForAnimations: { value: true, from: "default" }, animationDistanceThreshold: { value: 5, from: "default" }, @@ -749,7 +746,7 @@ describe "lib/config", -> }) it "sets config, envFile and env", -> - @sandbox.stub(config, "getProcessEnvVars").returns({quux: "quux"}) + sinon.stub(config, "getProcessEnvVars").returns({quux: "quux"}) obj = { projectRoot: "/foo/bar" @@ -784,10 +781,10 @@ describe "lib/config", -> requestTimeout: { value: 5000, from: "default" }, responseTimeout: { value: 30000, from: "default" }, execTimeout: { value: 60000, from: "default" }, + taskTimeout: { value: 60000, from: "default" }, numTestsKeptInMemory: { value: 50, from: "default" }, waitForAnimations: { value: true, from: "default" }, animationDistanceThreshold: { value: 5, from: "default" }, - screenshotOnHeadlessFailure:{ value: true, from: "default" }, trashAssetsBeforeHeadlessRuns: { value: true, from: "default" }, watchForFileChanges: { value: true, from: "default" }, modifyObstructiveCode: { value: true, from: "default" }, @@ -882,7 +879,7 @@ describe "lib/config", -> context ".parseEnv", -> it "merges together env from config, env from file, env from process, and env from CLI", -> - @sandbox.stub(config, "getProcessEnvVars").returns({version: "0.12.1", user: "bob"}) + sinon.stub(config, "getProcessEnvVars").returns({version: "0.12.1", user: "bob"}) obj = { env: { @@ -970,16 +967,16 @@ describe "lib/config", -> expect(urls.proxyUrl).to.eq("http://localhost:65432") context ".setScaffoldPaths", -> - it "sets integrationExampleFile + integrationExampleName + scaffoldedFiles", -> + it "sets integrationExamplePath + integrationExampleName + scaffoldedFiles", -> obj = { integrationFolder: "/_test-output/path/to/project/cypress/integration" } - @sandbox.stub(scaffold, "fileTree").returns([]) + sinon.stub(scaffold, "fileTree").returns([]) expect(config.setScaffoldPaths(obj)).to.deep.eq({ integrationFolder: "/_test-output/path/to/project/cypress/integration" - integrationExampleFile: "/_test-output/path/to/project/cypress/integration/example_spec.js" - integrationExampleName: "example_spec.js" + integrationExamplePath: "/_test-output/path/to/project/cypress/integration/examples" + integrationExampleName: "examples" scaffoldedFiles: [] }) diff --git a/packages/server/test/unit/environment_spec.coffee b/packages/server/test/unit/environment_spec.coffee index a3e2e37559a7..c6f7584cc930 100644 --- a/packages/server/test/unit/environment_spec.coffee +++ b/packages/server/test/unit/environment_spec.coffee @@ -2,61 +2,61 @@ require("../spec_helper") Promise = require("bluebird") pkg = require("@packages/root") +fs = require("#{root}lib/util/fs") -describe "lib/environment", -> - before -> - @env = process.env["CYPRESS_ENV"] +setEnv = (env) => + process.env["CYPRESS_ENV"] = env + expectedEnv(env) - beforeEach -> - @sandbox.stub(Promise, "config") +expectedEnv = (env) -> + require("#{root}lib/environment") + expect(process.env["CYPRESS_ENV"]).to.eq(env) + +setPkg = (env) => + pkg.env = env + expectedEnv(env) - @expectedEnv = (env) -> - require("#{root}lib/environment") - expect(process.env["CYPRESS_ENV"]).to.eq(env) +env = process.env["CYPRESS_ENV"] + +describe "lib/environment", -> + beforeEach -> + sinon.stub(Promise, "config") + delete process.env["CYPRESS_ENV"] + delete require.cache[require.resolve("#{root}lib/environment")] afterEach -> delete require.cache[require.resolve("#{root}lib/environment")] delete process.env["CYPRESS_ENV"] after -> - process.env["CYPRESS_ENV"] = @env + process.env["CYPRESS_ENV"] = env context "#existing process.env.CYPRESS_ENV", -> - beforeEach -> - @setEnv = (env) => - process.env["CYPRESS_ENV"] = env - @expectedEnv(env) - it "is production", -> - @setEnv("production") + setEnv("production") it "is development", -> - @setEnv("development") + setEnv("development") it "is staging", -> - @setEnv("staging") + setEnv("staging") context "uses package.json env", -> - beforeEach -> - @setEnv = (env) => - pkg.env = env - @expectedEnv(env) - afterEach -> delete pkg.env it "is production", -> - @setEnv("production") + setPkg("production") it "is staging", -> - @setEnv("staging") + setPkg("staging") it "is test", -> - @setEnv("test") + setPkg("test") context "it uses development by default", -> beforeEach -> - @sandbox.stub(fs, "readJsonSync").returns({}) + sinon.stub(fs, "readJsonSync").returns({}) it "is development", -> - @expectedEnv("development") + expectedEnv("development") diff --git a/packages/server/test/unit/errors_spec.coffee b/packages/server/test/unit/errors_spec.coffee index 9762a3d3a50f..187525a67eab 100644 --- a/packages/server/test/unit/errors_spec.coffee +++ b/packages/server/test/unit/errors_spec.coffee @@ -8,7 +8,7 @@ logger = require("#{root}lib/logger") describe "lib/errors", -> beforeEach -> @env = process.env.CYPRESS_ENV - @log = @sandbox.stub(console, "log") + @log = sinon.stub(console, "log") afterEach -> process.env.CYPRESS_ENV = @env @@ -43,7 +43,7 @@ describe "lib/errors", -> expect(@log).to.be.calledWith(chalk.red(foo.stack)) it "calls logger.createException", -> - @sandbox.stub(logger, "createException").resolves() + sinon.stub(logger, "createException").resolves() process.env.CYPRESS_ENV = "production" diff --git a/packages/server/test/unit/exceptions_spec.coffee b/packages/server/test/unit/exceptions_spec.coffee index 410c8320ed26..b0dca8bfba52 100644 --- a/packages/server/test/unit/exceptions_spec.coffee +++ b/packages/server/test/unit/exceptions_spec.coffee @@ -16,13 +16,13 @@ pkg = require("@packages/root") describe "lib/exceptions", -> context ".getAuthToken", -> it "returns authToken from cache", -> - @sandbox.stub(user, "get").resolves({authToken: "auth-token-123"}) + sinon.stub(user, "get").resolves({authToken: "auth-token-123"}) exception.getAuthToken().then (authToken) -> expect(authToken).to.eq("auth-token-123") it "returns undefined if no authToken", -> - @sandbox.stub(user, "get").resolves({}) + sinon.stub(user, "get").resolves({}) exception.getAuthToken().then (authToken) -> expect(authToken).to.be.undinefed @@ -82,14 +82,14 @@ describe "lib/exceptions", -> context ".getVersion", -> it "returns version from package.json", -> - @sandbox.stub(pkg, "version", "0.1.2") + sinon.stub(pkg, "version").value("0.1.2") expect(exception.getVersion()).to.eq("0.1.2") context ".getBody", -> beforeEach -> @err = new Error() - @sandbox.stub(pkg, "version", "0.1.2") - @sandbox.stub(system, "info").resolves({ + sinon.stub(pkg, "version").value("0.1.2") + sinon.stub(system, "info").resolves({ system: "info" }) @@ -125,14 +125,14 @@ describe "lib/exceptions", -> @err = {name: "ReferenceError", message: "undefined is not a function", stack: "asfd"} - @sandbox.stub(exception, "getBody").resolves({ + sinon.stub(exception, "getBody").resolves({ err: @err version: "0.1.2" }) - @sandbox.stub(exception, "getAuthToken").resolves("auth-token-123") + sinon.stub(exception, "getAuthToken").resolves("auth-token-123") - @sandbox.stub(api, "createRaygunException") + sinon.stub(api, "createRaygunException") it "sends body + authToken to api.createRaygunException", -> api.createRaygunException.resolves() diff --git a/packages/server/test/unit/exec_spec.coffee b/packages/server/test/unit/exec_spec.coffee index 1534d94075ed..901926faf2ad 100644 --- a/packages/server/test/unit/exec_spec.coffee +++ b/packages/server/test/unit/exec_spec.coffee @@ -111,7 +111,7 @@ describe "lib/exec", -> .catch (err) -> expect(err.message).to.include "Process timed out" expect(err.message).to.include "command: pause" - expect(err.timedout).to.be.true + expect(err.timedOut).to.be.true context "#linux / mac", -> return if isWindows() @@ -181,4 +181,4 @@ describe "lib/exec", -> .catch (err) -> expect(err.message).to.include "Process timed out" expect(err.message).to.include "command: sleep 2" - expect(err.timedout).to.be.true + expect(err.timedOut).to.be.true diff --git a/packages/server/test/unit/file_spec.coffee b/packages/server/test/unit/file_spec.coffee index e70744aef06e..26f6dd58c53a 100644 --- a/packages/server/test/unit/file_spec.coffee +++ b/packages/server/test/unit/file_spec.coffee @@ -3,15 +3,16 @@ require("../spec_helper") os = require("os") path = require("path") Promise = require("bluebird") -FileUtil = require("#{root}lib/util/file") -exit = require("#{root}lib/util/exit") lockFile = Promise.promisifyAll(require("lockfile")) -fs = Promise.promisifyAll(require("fs-extra")) +fs = require("#{root}lib/util/fs") +exit = require("#{root}lib/util/exit") +FileUtil = require("#{root}lib/util/file") describe "lib/util/file", -> beforeEach -> @dir = path.join(os.tmpdir(), "cypress", "file_spec") @path = path.join(@dir, "file.json") + fs.removeAsync(@dir).catch -> ## ignore error if directory didn't exist in the first place @@ -19,8 +20,8 @@ describe "lib/util/file", -> expect(-> new FileUtil()).to.throw("Must specify path to file when creating new FileUtil()") it "unlocks file on exit", -> - @sandbox.spy(lockFile, "unlockSync") - @sandbox.stub(exit, "ensure") + sinon.spy(lockFile, "unlockSync") + sinon.stub(exit, "ensure") new FileUtil({path: @path}) exit.ensure.yield() expect(lockFile.unlockSync).to.be.called @@ -106,7 +107,7 @@ describe "lib/util/file", -> .then => fs.writeJsonAsync(@path, {foo: "bar"}) .then => - @sandbox.stub(lockFile, "lockAsync").rejects({name: "", message: "", code: "EEXIST"}) + sinon.stub(lockFile, "lockAsync").rejects({name: "", message: "", code: "EEXIST"}) @fileUtil.get() .then (contents) -> expect(contents).to.eql({}) @@ -114,7 +115,7 @@ describe "lib/util/file", -> it "resolves cached contents when it can't get lock on file after an initial read", -> @fileUtil.set("foo", "bar") .then => - @sandbox.stub(lockFile, "lockAsync").rejects({name: "", message: "", code: "EEXIST"}) + sinon.stub(lockFile, "lockAsync").rejects({name: "", message: "", code: "EEXIST"}) @fileUtil.get() .then (contents) -> expect(contents).to.eql({foo: "bar"}) @@ -129,7 +130,7 @@ describe "lib/util/file", -> expect(contents).to.eql({}) it "debounces reading from disk", -> - @sandbox.stub(fs, "readJsonAsync").resolves({}) + sinon.stub(fs, "readJsonAsync").resolves({}) Promise.all([ @fileUtil.get() @fileUtil.get() @@ -139,18 +140,18 @@ describe "lib/util/file", -> expect(fs.readJsonAsync).to.be.calledOnce it "locks file while reading", -> - @sandbox.spy(lockFile, "lockAsync") + sinon.spy(lockFile, "lockAsync") @fileUtil.get().then -> expect(lockFile.lockAsync).to.be.called it "unlocks file when finished reading", -> - @sandbox.spy(lockFile, "unlockAsync") + sinon.spy(lockFile, "unlockAsync") @fileUtil.get().then -> expect(lockFile.unlockAsync).to.be.called it "unlocks file even if reading fails", -> - @sandbox.spy(lockFile, "unlockAsync") - @sandbox.stub(fs, "readJsonAsync").rejects(new Error("fail!")) + sinon.spy(lockFile, "unlockAsync") + sinon.stub(fs, "readJsonAsync").rejects(new Error("fail!")) @fileUtil.get().catch -> expect(lockFile.unlockAsync).to.be.called @@ -217,18 +218,18 @@ describe "lib/util/file", -> expect(JSON.parse(contents)).to.eql({foo: "bar"}) it "locks file while writing", -> - @sandbox.spy(lockFile, "lockAsync") + sinon.spy(lockFile, "lockAsync") @fileUtil.set("foo", "bar").then -> expect(lockFile.lockAsync).to.be.called it "unlocks file when finished writing", -> - @sandbox.spy(lockFile, "unlockAsync") + sinon.spy(lockFile, "unlockAsync") @fileUtil.set("foo", "bar").then -> expect(lockFile.unlockAsync).to.be.called it "unlocks file even if writing fails", -> - @sandbox.spy(lockFile, "unlockAsync") - @sandbox.stub(fs, "outputJsonAsync").rejects(new Error("fail!")) + sinon.spy(lockFile, "unlockAsync") + sinon.stub(fs, "outputJsonAsync").rejects(new Error("fail!")) @fileUtil.set("foo", "bar").catch -> expect(lockFile.unlockAsync).to.be.called @@ -243,17 +244,23 @@ describe "lib/util/file", -> .catch -> it "locks file while removing", -> - @sandbox.spy(lockFile, "lockAsync") + sinon.spy(lockFile, "lockAsync") @fileUtil.remove().then -> expect(lockFile.lockAsync).to.be.called it "unlocks file when finished removing", -> - @sandbox.spy(lockFile, "unlockAsync") - @fileUtil.remove().then -> + sinon.spy(lockFile, "unlockAsync") + @fileUtil.remove() + .then -> expect(lockFile.unlockAsync).to.be.called it "unlocks file even if removing fails", -> - @sandbox.spy(lockFile, "unlockAsync") - @sandbox.stub(fs, "removeAsync").rejects(new Error("fail!")) - @fileUtil.remove().catch -> + sinon.spy(lockFile, "unlockAsync") + sinon.stub(fs, "removeAsync").rejects(new Error("fail!")) + + @fileUtil.remove() + .then -> + throw new Error("should have caught!") + .catch (err) -> + expect(err.message).to.eq("fail!") expect(lockFile.unlockAsync).to.be.called diff --git a/packages/server/test/unit/files_spec.coffee b/packages/server/test/unit/files_spec.coffee index 4b7c9d489da4..e38cfefe9298 100644 --- a/packages/server/test/unit/files_spec.coffee +++ b/packages/server/test/unit/files_spec.coffee @@ -1,64 +1,10 @@ require("../spec_helper") -Promise = require("bluebird") -human = require("human-interval") -path = require("path") -R = require("ramda") - -api = require("#{root}lib/api") config = require("#{root}lib/config") -user = require("#{root}lib/user") files = require("#{root}lib/files") FixturesHelper = require("#{root}/test/support/helpers/fixtures") filesController = require("#{root}lib/controllers/files") -describe "lib/controllers/files", -> - beforeEach -> - FixturesHelper.scaffold() - - @todosPath = FixturesHelper.projectPath("todos") - - config.get(@todosPath).then (cfg) => - @config = cfg - - afterEach -> - FixturesHelper.remove() - - context "#getTestFiles", -> - - checkFoundSpec = (foundSpec) -> - if not path.isAbsolute(foundSpec.absolute) - throw new Error("path to found spec should be absolute #{JSON.stringify(foundSpec)}") - - it "returns absolute filenames", -> - filesController - .getTestFiles(@config) - .then (R.prop("integration")) - .then (R.forEach(checkFoundSpec)) - - it "handles fixturesFolder being false", -> - @config.fixturesFolder = false - expect(=> filesController.getTestFiles(@config)).not.to.throw() - - it "by default, returns all files as long as they have a name and extension", -> - config.get(FixturesHelper.projectPath("various-file-types")) - .then (cfg) -> - filesController.getTestFiles(cfg) - .then (files) -> - expect(files.integration.length).to.equal(3) - expect(files.integration[0].name).to.equal("coffee_spec.coffee") - expect(files.integration[1].name).to.equal("js_spec.js") - expect(files.integration[2].name).to.equal("ts_spec.ts") - - it "returns files matching config.testFiles", -> - config.get(FixturesHelper.projectPath("various-file-types")) - .then (cfg) -> - cfg.testFiles = "**/*.coffee" - filesController.getTestFiles(cfg) - .then (files) -> - expect(files.integration.length).to.equal(1) - expect(files.integration[0].name).to.equal("coffee_spec.coffee") - describe "lib/files", -> beforeEach -> FixturesHelper.scaffold() @@ -73,7 +19,6 @@ describe "lib/files", -> FixturesHelper.remove() context "#readFile", -> - it "returns contents and full file path", -> files.readFile(@projectRoot, "tests/_fixtures/message.txt").then ({ contents, filePath }) -> expect(contents).to.eq "foobarbaz" @@ -100,7 +45,6 @@ describe "lib/files", -> ] context "#writeFile", -> - it "writes the file's contents and returns contents and full file path", -> files.writeFile(@projectRoot, ".projects/write_file.txt", "foo").then => files.readFile(@projectRoot, ".projects/write_file.txt").then ({ contents, filePath }) -> diff --git a/packages/server/test/unit/fixture_spec.coffee b/packages/server/test/unit/fixture_spec.coffee index 2e5c3f74eed9..2bf082cf1c9c 100644 --- a/packages/server/test/unit/fixture_spec.coffee +++ b/packages/server/test/unit/fixture_spec.coffee @@ -1,10 +1,10 @@ require("../spec_helper") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") config = require("#{root}lib/config") fixture = require("#{root}lib/fixture") +fs = require("#{root}lib/util/fs") FixturesHelper = require("#{root}/test/support/helpers/fixtures") os = require("os") eol = require("eol") @@ -12,8 +12,6 @@ eol = require("eol") isWindows = () -> os.platform() == "win32" -fs = Promise.promisifyAll(fs) - describe "lib/fixture", -> beforeEach -> FixturesHelper.scaffold() diff --git a/packages/server/test/unit/misc_spec.coffee b/packages/server/test/unit/fs_spec.coffee similarity index 86% rename from packages/server/test/unit/misc_spec.coffee rename to packages/server/test/unit/fs_spec.coffee index 89f8147ad79b..b76675551afc 100644 --- a/packages/server/test/unit/misc_spec.coffee +++ b/packages/server/test/unit/fs_spec.coffee @@ -1,8 +1,10 @@ require("../spec_helper") -describe "misc tests", -> +fs = require("#{root}lib/util/fs") + +describe "lib/util/fs", -> beforeEach () -> - @sandbox.spy(console, "error") + sinon.spy(console, "error") it "warns when trying to use fs.existsSync", -> fs.existsSync(__filename) diff --git a/packages/server/test/unit/gui/dialog_spec.coffee b/packages/server/test/unit/gui/dialog_spec.coffee index 97bf9c37c8a6..43a5d3eecdca 100644 --- a/packages/server/test/unit/gui/dialog_spec.coffee +++ b/packages/server/test/unit/gui/dialog_spec.coffee @@ -7,7 +7,7 @@ Windows = require("#{root}../lib/gui/windows") describe "gui/dialog", -> context ".show", -> beforeEach -> - @showOpenDialog = electron.dialog.showOpenDialog = @sandbox.stub() + @showOpenDialog = electron.dialog.showOpenDialog = sinon.stub() it "calls dialog.showOpenDialog with args", -> dialog.show() diff --git a/packages/server/test/unit/gui/events_spec.coffee b/packages/server/test/unit/gui/events_spec.coffee index 177ae71b37dd..097475ed5c7c 100644 --- a/packages/server/test/unit/gui/events_spec.coffee +++ b/packages/server/test/unit/gui/events_spec.coffee @@ -23,9 +23,9 @@ konfig = require("#{root}../lib/konfig") describe "lib/gui/events", -> beforeEach -> - @send = @sandbox.spy() + @send = sinon.spy() @options = {} - @cookies = @sandbox.stub({ + @cookies = sinon.stub({ get: -> set: -> remove: -> @@ -40,8 +40,8 @@ describe "lib/gui/events", -> } @bus = new EE() - @sandbox.stub(electron.ipcMain, "on") - @sandbox.stub(electron.ipcMain, "removeAllListeners") + sinon.stub(electron.ipcMain, "on") + sinon.stub(electron.ipcMain, "removeAllListeners") @handleEvent = (type, arg) => id = "#{type}-#{Math.random()}" @@ -62,13 +62,13 @@ describe "lib/gui/events", -> context ".start", -> it "ipc attaches callback on request", -> - handleEvent = @sandbox.stub(events, "handleEvent") + handleEvent = sinon.stub(events, "handleEvent") events.start({foo: "bar"}) expect(electron.ipcMain.on).to.be.calledWith("request") it "partials in options in request callback", -> electron.ipcMain.on.yields("arg1", "arg2") - handleEvent = @sandbox.stub(events, "handleEvent") + handleEvent = sinon.stub(events, "handleEvent") events.start({foo: "bar"}, {}) expect(handleEvent).to.be.calledWith({foo: "bar"}, {}, "arg1", "arg2") @@ -81,13 +81,13 @@ describe "lib/gui/events", -> context "dialog", -> describe "show:directory:dialog", -> it "calls dialog.show and returns", -> - @sandbox.stub(dialog, "show").resolves({foo: "bar"}) + sinon.stub(dialog, "show").resolves({foo: "bar"}) @handleEvent("show:directory:dialog").then (assert) => assert.sendCalledWith({foo: "bar"}) it "catches errors", -> err = new Error("foo") - @sandbox.stub(dialog, "show").rejects(err) + sinon.stub(dialog, "show").rejects(err) @handleEvent("show:directory:dialog").then (assert) => assert.sendErrCalledWith(err) @@ -95,39 +95,39 @@ describe "lib/gui/events", -> context "user", -> describe "log:in", -> it "calls user.logIn and returns user", -> - @sandbox.stub(user, "logIn").withArgs("12345").resolves({foo: "bar"}) + sinon.stub(user, "logIn").withArgs("12345").resolves({foo: "bar"}) @handleEvent("log:in", "12345").then (assert) => assert.sendCalledWith({foo: "bar"}) it "catches errors", -> err = new Error("foo") - @sandbox.stub(user, "logIn").rejects(err) + sinon.stub(user, "logIn").rejects(err) @handleEvent("log:in").then (assert) => assert.sendErrCalledWith(err) describe "log:out", -> it "calls user.logOut and returns user", -> - @sandbox.stub(user, "logOut").resolves({foo: "bar"}) + sinon.stub(user, "logOut").resolves({foo: "bar"}) @handleEvent("log:out").then (assert) => assert.sendCalledWith({foo: "bar"}) it "catches errors", -> err = new Error("foo") - @sandbox.stub(user, "logOut").rejects(err) + sinon.stub(user, "logOut").rejects(err) @handleEvent("log:out").then (assert) => assert.sendErrCalledWith(err) describe "get:current:user", -> it "calls user.get and returns user", -> - @sandbox.stub(user, "get").resolves({foo: "bar"}) + sinon.stub(user, "get").resolves({foo: "bar"}) @handleEvent("get:current:user").then (assert) => assert.sendCalledWith({foo: "bar"}) it "catches errors", -> err = new Error("foo") - @sandbox.stub(user, "get").rejects(err) + sinon.stub(user, "get").rejects(err) @handleEvent("get:current:user").then (assert) => assert.sendErrCalledWith(err) @@ -135,10 +135,10 @@ describe "lib/gui/events", -> context "cookies", -> describe "clear:github:cookies", -> it "clears cookies and returns null", -> - @sandbox.stub(Windows, "getBrowserAutomation") + sinon.stub(Windows, "getBrowserAutomation") .withArgs(@event.sender) .returns({ - clearCookies: @sandbox.stub().withArgs({domain: "github.com"}).resolves() + clearCookies: sinon.stub().withArgs({domain: "github.com"}).resolves() }) @handleEvent("clear:github:cookies").then (assert) => @@ -147,10 +147,10 @@ describe "lib/gui/events", -> it "catches errors", -> err = new Error("foo") - @sandbox.stub(Windows, "getBrowserAutomation") + sinon.stub(Windows, "getBrowserAutomation") .withArgs(@event.sender) .returns({ - clearCookies: @sandbox.stub().withArgs({domain: "github.com"}).rejects(err) + clearCookies: sinon.stub().withArgs({domain: "github.com"}).rejects(err) }) @handleEvent("clear:github:cookies", {foo: "bar"}).then (assert) => @@ -159,23 +159,23 @@ describe "lib/gui/events", -> context "external shell", -> describe "external:open", -> it "shell.openExternal with arg", -> - electron.shell.openExternal = @sandbox.spy() + electron.shell.openExternal = sinon.spy() @handleEvent("external:open", {foo: "bar"}).then -> expect(electron.shell.openExternal).to.be.calledWith({foo: "bar"}) context "window", -> describe "window:open", -> beforeEach -> - @options.projectPath = "/path/to/my/project" + @options.projectRoot = "/path/to/my/project" - @win = @sandbox.stub({ + @win = sinon.stub({ on: -> once: -> loadURL: -> webContents: {} }) - @sandbox.stub(Windows, "create").withArgs(@options.projectPath).returns(@win) + sinon.stub(Windows, "create").withArgs(@options.projectRoot).returns(@win) it "calls Windows#open with args and resolves with return of Windows.open", -> @handleEvent("window:open", {type: "INDEX"}) @@ -184,69 +184,69 @@ describe "lib/gui/events", -> it "catches errors", -> err = new Error("foo") - @sandbox.stub(Windows, "open").withArgs(@options.projectPath, {foo: "bar"}).rejects(err) + sinon.stub(Windows, "open").withArgs(@options.projectRoot, {foo: "bar"}).rejects(err) @handleEvent("window:open", {foo: "bar"}).then (assert) => assert.sendErrCalledWith(err) describe "window:close", -> it "calls destroy on Windows#getByWebContents", -> - @destroy = @sandbox.stub() - @sandbox.stub(Windows, "getByWebContents").withArgs(@event.sender).returns({destroy: @destroy}) + @destroy = sinon.stub() + sinon.stub(Windows, "getByWebContents").withArgs(@event.sender).returns({destroy: @destroy}) @handleEvent("window:close") expect(@destroy).to.be.calledOnce context "updating", -> describe "updater:check", -> it "returns version when new version", -> - @sandbox.stub(Updater, "check").yieldsTo("onNewVersion", {version: "1.2.3"}) + sinon.stub(Updater, "check").yieldsTo("onNewVersion", {version: "1.2.3"}) @handleEvent("updater:check").then (assert) -> assert.sendCalledWith("1.2.3") it "returns false when no new version", -> - @sandbox.stub(Updater, "check").yieldsTo("onNoNewVersion") + sinon.stub(Updater, "check").yieldsTo("onNoNewVersion") @handleEvent("updater:check").then (assert) -> assert.sendCalledWith(false) context "log events", -> describe "get:logs", -> it "returns array of logs", -> - @sandbox.stub(logger, "getLogs").resolves([]) + sinon.stub(logger, "getLogs").resolves([]) @handleEvent("get:logs").then (assert) => assert.sendCalledWith([]) it "catches errors", -> err = new Error("foo") - @sandbox.stub(logger, "getLogs").rejects(err) + sinon.stub(logger, "getLogs").rejects(err) @handleEvent("get:logs").then (assert) => assert.sendErrCalledWith(err) describe "clear:logs", -> it "returns null", -> - @sandbox.stub(logger, "clearLogs").resolves() + sinon.stub(logger, "clearLogs").resolves() @handleEvent("clear:logs").then (assert) => assert.sendCalledWith(null) it "catches errors", -> err = new Error("foo") - @sandbox.stub(logger, "clearLogs").rejects(err) + sinon.stub(logger, "clearLogs").rejects(err) @handleEvent("clear:logs").then (assert) => assert.sendErrCalledWith(err) describe "on:log", -> it "sets send to onLog", -> - onLog = @sandbox.stub(logger, "onLog") + onLog = sinon.stub(logger, "onLog") @handleEvent("on:log") expect(onLog).to.be.called expect(onLog.getCall(0).args[0]).to.be.a("function") describe "off:log", -> it "calls logger#off and returns null", -> - @sandbox.stub(logger, "off") + sinon.stub(logger, "off") @handleEvent("off:log").then (assert) -> expect(logger.off).to.be.calledOnce assert.sendCalledWith(null) @@ -256,7 +256,7 @@ describe "lib/gui/events", -> it "calls logs.error with arg", -> err = new Error("foo") - @sandbox.stub(logs, "error").withArgs(err).resolves() + sinon.stub(logs, "error").withArgs(err).resolves() @handleEvent("gui:error", err).then (assert) => assert.sendCalledWith(null) @@ -264,7 +264,7 @@ describe "lib/gui/events", -> it "calls logger.createException with error", -> err = new Error("foo") - @sandbox.stub(logger, "createException").withArgs(err).resolves() + sinon.stub(logger, "createException").withArgs(err).resolves() @handleEvent("gui:error", err).then (assert) => expect(logger.createException).to.be.calledOnce @@ -273,7 +273,7 @@ describe "lib/gui/events", -> it "swallows logger.createException errors", -> err = new Error("foo") - @sandbox.stub(logger, "createException").withArgs(err).rejects(new Error("err")) + sinon.stub(logger, "createException").withArgs(err).rejects(new Error("err")) @handleEvent("gui:error", err).then (assert) => expect(logger.createException).to.be.calledOnce @@ -283,7 +283,7 @@ describe "lib/gui/events", -> err = new Error("foo") err2 = new Error("bar") - @sandbox.stub(logs, "error").withArgs(err).rejects(err2) + sinon.stub(logs, "error").withArgs(err).rejects(err2) @handleEvent("gui:error", err).then (assert) => assert.sendErrCalledWith(err2) @@ -291,21 +291,21 @@ describe "lib/gui/events", -> context "user events", -> describe "get:orgs", -> it "returns array of orgs", -> - @sandbox.stub(Project, "getOrgs").resolves([]) + sinon.stub(Project, "getOrgs").resolves([]) @handleEvent("get:orgs").then (assert) => assert.sendCalledWith([]) it "catches errors", -> err = new Error("foo") - @sandbox.stub(Project, "getOrgs").rejects(err) + sinon.stub(Project, "getOrgs").rejects(err) @handleEvent("get:orgs").then (assert) => assert.sendErrCalledWith(err) describe "open:finder", -> it "opens with open lib", -> - @sandbox.stub(open, "opn").resolves("okay") + sinon.stub(open, "opn").resolves("okay") @handleEvent("open:finder", "path").then (assert) => expect(open.opn).to.be.calledWith("path") @@ -313,15 +313,15 @@ describe "lib/gui/events", -> it "catches errors", -> err = new Error("foo") - @sandbox.stub(open, "opn").rejects(err) + sinon.stub(open, "opn").rejects(err) @handleEvent("open:finder", "path").then (assert) => assert.sendErrCalledWith(err) it "works even after project is opened (issue #227)", -> - @sandbox.stub(open, "opn").resolves("okay") - @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + sinon.stub(open, "opn").resolves("okay") + sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -333,79 +333,79 @@ describe "lib/gui/events", -> context "project events", -> describe "get:projects", -> it "returns array of projects", -> - @sandbox.stub(Project, "getPathsAndIds").resolves([]) + sinon.stub(Project, "getPathsAndIds").resolves([]) @handleEvent("get:projects").then (assert) => assert.sendCalledWith([]) it "catches errors", -> err = new Error("foo") - @sandbox.stub(Project, "getPathsAndIds").rejects(err) + sinon.stub(Project, "getPathsAndIds").rejects(err) @handleEvent("get:projects").then (assert) => assert.sendErrCalledWith(err) describe "get:project:statuses", -> it "returns array of projects with statuses", -> - @sandbox.stub(Project, "getProjectStatuses").resolves([]) + sinon.stub(Project, "getProjectStatuses").resolves([]) @handleEvent("get:project:statuses").then (assert) => assert.sendCalledWith([]) it "catches errors", -> err = new Error("foo") - @sandbox.stub(Project, "getProjectStatuses").rejects(err) + sinon.stub(Project, "getProjectStatuses").rejects(err) @handleEvent("get:project:statuses").then (assert) => assert.sendErrCalledWith(err) describe "get:project:status", -> it "returns project returned by Project.getProjectStatus", -> - @sandbox.stub(Project, "getProjectStatus").resolves("project") + sinon.stub(Project, "getProjectStatus").resolves("project") @handleEvent("get:project:status").then (assert) => assert.sendCalledWith("project") it "catches errors", -> err = new Error("foo") - @sandbox.stub(Project, "getProjectStatus").rejects(err) + sinon.stub(Project, "getProjectStatus").rejects(err) @handleEvent("get:project:status").then (assert) => assert.sendErrCalledWith(err) describe "add:project", -> it "adds project + returns result", -> - @sandbox.stub(Project, "add").withArgs("/_test-output/path/to/project").resolves("result") + sinon.stub(Project, "add").withArgs("/_test-output/path/to/project").resolves("result") @handleEvent("add:project", "/_test-output/path/to/project").then (assert) => assert.sendCalledWith("result") it "catches errors", -> err = new Error("foo") - @sandbox.stub(Project, "add").withArgs("/_test-output/path/to/project").rejects(err) + sinon.stub(Project, "add").withArgs("/_test-output/path/to/project").rejects(err) @handleEvent("add:project", "/_test-output/path/to/project").then (assert) => assert.sendErrCalledWith(err) describe "remove:project", -> it "remove project + returns arg", -> - @sandbox.stub(cache, "removeProject").withArgs("/_test-output/path/to/project").resolves() + sinon.stub(cache, "removeProject").withArgs("/_test-output/path/to/project").resolves() @handleEvent("remove:project", "/_test-output/path/to/project").then (assert) => assert.sendCalledWith("/_test-output/path/to/project") it "catches errors", -> err = new Error("foo") - @sandbox.stub(cache, "removeProject").withArgs("/_test-output/path/to/project").rejects(err) + sinon.stub(cache, "removeProject").withArgs("/_test-output/path/to/project").rejects(err) @handleEvent("remove:project", "/_test-output/path/to/project").then (assert) => assert.sendErrCalledWith(err) describe "open:project", -> beforeEach -> - @sandbox.stub(extension, "setHostAndPath").resolves() - @sandbox.stub(browsers, "get").resolves([]) - @sandbox.stub(Project.prototype, "close").resolves() + sinon.stub(extension, "setHostAndPath").resolves() + sinon.stub(browsers, "get").resolves([]) + sinon.stub(Project.prototype, "close").resolves() afterEach -> ## close down 'open' projects @@ -413,8 +413,8 @@ describe "lib/gui/events", -> openProject.close() it "open project + returns config", -> - @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then (assert) => @@ -422,15 +422,15 @@ describe "lib/gui/events", -> it "catches errors", -> err = new Error("foo") - @sandbox.stub(Project.prototype, "open").rejects(err) + sinon.stub(Project.prototype, "open").rejects(err) @handleEvent("open:project", "/_test-output/path/to/project") .then (assert) => assert.sendErrCalledWith(err) it "sends 'focus:tests' onFocusTests", -> - open = @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + open = sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -440,8 +440,8 @@ describe "lib/gui/events", -> assert.sendCalledWith(undefined) it "sends 'config:changed' onSettingsChanged", -> - open = @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + open = sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -451,8 +451,8 @@ describe "lib/gui/events", -> assert.sendCalledWith(undefined) it "sends 'spec:changed' onSpecChanged", -> - open = @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + open = sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -462,8 +462,8 @@ describe "lib/gui/events", -> assert.sendCalledWith("/path/to/spec.coffee") it "sends 'project:warning' onWarning", -> - open = @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + open = sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -473,8 +473,8 @@ describe "lib/gui/events", -> assert.sendCalledWith({name: "foo", message: "foo"}) it "sends 'project:error' onError", -> - open = @sandbox.stub(Project.prototype, "open") - @sandbox.stub(Project.prototype, "getConfig").resolves({some: "config"}) + open = sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "getConfig").resolves({some: "config"}) @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -485,7 +485,7 @@ describe "lib/gui/events", -> describe "close:project", -> beforeEach -> - @sandbox.stub(Project.prototype, "close").withArgs({sync: true}).resolves() + sinon.stub(Project.prototype, "close").withArgs({sync: true}).resolves() it "is noop and returns null when no project is open", -> expect(openProject.getProject()).to.be.null @@ -494,8 +494,8 @@ describe "lib/gui/events", -> assert.sendCalledWith(null) it "closes down open project and returns null", -> - @sandbox.stub(Project.prototype, "getConfig").resolves({}) - @sandbox.stub(Project.prototype, "open").withArgs({sync: true}).resolves() + sinon.stub(Project.prototype, "getConfig").resolves({}) + sinon.stub(Project.prototype, "open").withArgs({sync: true}).resolves() @handleEvent("open:project", "/_test-output/path/to/project") .then => @@ -511,13 +511,13 @@ describe "lib/gui/events", -> describe "get:runs", -> it "calls openProject.getRuns", -> - @sandbox.stub(openProject, "getRuns").resolves([]) + sinon.stub(openProject, "getRuns").resolves([]) @handleEvent("get:runs").then (assert) => expect(openProject.getRuns).to.be.called it "returns array of runs", -> - @sandbox.stub(openProject, "getRuns").resolves([]) + sinon.stub(openProject, "getRuns").resolves([]) @handleEvent("get:runs").then (assert) => assert.sendCalledWith([]) @@ -525,7 +525,7 @@ describe "lib/gui/events", -> it "sends UNAUTHENTICATED when statusCode is 401", -> err = new Error("foo") err.statusCode = 401 - @sandbox.stub(openProject, "getRuns").rejects(err) + sinon.stub(openProject, "getRuns").rejects(err) @handleEvent("get:runs").then (assert) => expect(@send).to.be.calledWith("response") @@ -534,7 +534,7 @@ describe "lib/gui/events", -> it "sends TIMED_OUT when cause.code is ESOCKETTIMEDOUT", -> err = new Error("foo") err.cause = { code: "ESOCKETTIMEDOUT" } - @sandbox.stub(openProject, "getRuns").rejects(err) + sinon.stub(openProject, "getRuns").rejects(err) @handleEvent("get:runs").then (assert) => expect(@send).to.be.calledWith("response") @@ -543,7 +543,7 @@ describe "lib/gui/events", -> it "sends NO_CONNECTION when code is ENOTFOUND", -> err = new Error("foo") err.code = "ENOTFOUND" - @sandbox.stub(openProject, "getRuns").rejects(err) + sinon.stub(openProject, "getRuns").rejects(err) @handleEvent("get:runs").then (assert) => expect(@send).to.be.calledWith("response") @@ -552,7 +552,7 @@ describe "lib/gui/events", -> it "sends type when if existing for other errors", -> err = new Error("foo") err.type = "NO_PROJECT_ID" - @sandbox.stub(openProject, "getRuns").rejects(err) + sinon.stub(openProject, "getRuns").rejects(err) @handleEvent("get:runs").then (assert) => expect(@send).to.be.calledWith("response") @@ -563,7 +563,7 @@ describe "lib/gui/events", -> err.name = "name" err.message = "message" err.stack = "stack" - @sandbox.stub(openProject, "getRuns").rejects(err) + sinon.stub(openProject, "getRuns").rejects(err) @handleEvent("get:runs").then (assert) => expect(@send).to.be.calledWith("response") @@ -571,35 +571,35 @@ describe "lib/gui/events", -> describe "setup:dashboard:project", -> it "returns result of openProject.createCiProject", -> - @sandbox.stub(openProject, "createCiProject").resolves("response") + sinon.stub(openProject, "createCiProject").resolves("response") @handleEvent("setup:dashboard:project").then (assert) => assert.sendCalledWith("response") it "catches errors", -> err = new Error("foo") - @sandbox.stub(openProject, "createCiProject").rejects(err) + sinon.stub(openProject, "createCiProject").rejects(err) @handleEvent("setup:dashboard:project").then (assert) => assert.sendErrCalledWith(err) describe "get:record:keys", -> it "returns result of project.getRecordKeys", -> - @sandbox.stub(openProject, "getRecordKeys").resolves(["ci-key-123"]) + sinon.stub(openProject, "getRecordKeys").resolves(["ci-key-123"]) @handleEvent("get:record:keys").then (assert) => assert.sendCalledWith(["ci-key-123"]) it "catches errors", -> err = new Error("foo") - @sandbox.stub(openProject, "getRecordKeys").rejects(err) + sinon.stub(openProject, "getRecordKeys").rejects(err) @handleEvent("get:record:keys").then (assert) => assert.sendErrCalledWith(err) describe "request:access", -> it "returns result of project.requestAccess", -> - @sandbox.stub(openProject, "requestAccess").resolves("response") + sinon.stub(openProject, "requestAccess").resolves("response") @handleEvent("request:access", "org-id-123").then (assert) => expect(openProject.requestAccess).to.be.calledWith("org-id-123") @@ -607,7 +607,7 @@ describe "lib/gui/events", -> it "catches errors", -> err = new Error("foo") - @sandbox.stub(openProject, "requestAccess").rejects(err) + sinon.stub(openProject, "requestAccess").rejects(err) @handleEvent("request:access", "org-id-123").then (assert) => assert.sendErrCalledWith(err) @@ -615,7 +615,7 @@ describe "lib/gui/events", -> it "sends ALREADY_MEMBER when statusCode is 403", -> err = new Error("foo") err.statusCode = 403 - @sandbox.stub(openProject, "requestAccess").rejects(err) + sinon.stub(openProject, "requestAccess").rejects(err) @handleEvent("request:access", "org-id-123").then (assert) => expect(@send).to.be.calledWith("response") @@ -628,7 +628,7 @@ describe "lib/gui/events", -> userId: [ "This User has an existing MembershipRequest to this Organization." ] } - @sandbox.stub(openProject, "requestAccess").rejects(err) + sinon.stub(openProject, "requestAccess").rejects(err) @handleEvent("request:access", "org-id-123").then (assert) => expect(@send).to.be.calledWith("response") @@ -637,7 +637,7 @@ describe "lib/gui/events", -> it "sends type when if existing for other errors", -> err = new Error("foo") err.type = "SOME_TYPE" - @sandbox.stub(openProject, "requestAccess").rejects(err) + sinon.stub(openProject, "requestAccess").rejects(err) @handleEvent("request:access", "org-id-123").then (assert) => expect(@send).to.be.calledWith("response") @@ -645,7 +645,7 @@ describe "lib/gui/events", -> it "sends UNKNOWN for other errors", -> err = new Error("foo") - @sandbox.stub(openProject, "requestAccess").rejects(err) + sinon.stub(openProject, "requestAccess").rejects(err) @handleEvent("request:access", "org-id-123").then (assert) => expect(@send).to.be.calledWith("response") @@ -653,7 +653,7 @@ describe "lib/gui/events", -> describe "ping:api:server", -> it "returns ensures url", -> - @sandbox.stub(connect, "ensureUrl").resolves() + sinon.stub(connect, "ensureUrl").resolves() @handleEvent("ping:api:server").then (assert) => expect(connect.ensureUrl).to.be.calledWith(konfig("api_url")) @@ -661,7 +661,7 @@ describe "lib/gui/events", -> it "catches errors", -> err = new Error("foo") - @sandbox.stub(connect, "ensureUrl").rejects(err) + sinon.stub(connect, "ensureUrl").rejects(err) @handleEvent("ping:api:server").then (assert) => assert.sendErrCalledWith(err) @@ -676,7 +676,7 @@ describe "lib/gui/events", -> address: "127.0.0.1" } err.length = 1 - @sandbox.stub(connect, "ensureUrl").rejects(err) + sinon.stub(connect, "ensureUrl").rejects(err) @handleEvent("ping:api:server").then (assert) => assert.sendErrCalledWith(err) diff --git a/packages/server/test/unit/gui/menu_spec.coffee b/packages/server/test/unit/gui/menu_spec.coffee index bd69c2d2dc07..37d738270d5b 100644 --- a/packages/server/test/unit/gui/menu_spec.coffee +++ b/packages/server/test/unit/gui/menu_spec.coffee @@ -18,10 +18,10 @@ getLabels = (menu) -> describe "gui/menu", -> beforeEach -> - @sandbox.stub(os, "platform").returns("darwin") - @sandbox.stub(electron.Menu, "buildFromTemplate") - @sandbox.stub(electron.Menu, "setApplicationMenu") - electron.shell.openExternal = @sandbox.stub() + sinon.stub(os, "platform").returns("darwin") + sinon.stub(electron.Menu, "buildFromTemplate") + sinon.stub(electron.Menu, "setApplicationMenu") + electron.shell.openExternal = sinon.stub() it "builds menu from template and sets it", -> electron.Menu.buildFromTemplate.returns("menu") @@ -59,7 +59,7 @@ describe "gui/menu", -> expect(getSubMenuItem(cyMenu, "Quit").accelerator).to.equal("Command+Q") it "exits process when Quit is clicked", -> - @sandbox.stub(process, "exit") + sinon.stub(process, "exit") menu.set() getSubMenuItem(getMenuItem("Cypress"), "Quit").click() expect(process.exit).to.be.calledWith(0) @@ -94,19 +94,19 @@ describe "gui/menu", -> expect(electron.shell.openExternal).to.be.calledWith("https://on.cypress.io/dashboard") it "opens app data directory when View App Data is clicked", -> - @sandbox.stub(open, "opn") + sinon.stub(open, "opn") menu.set() getSubMenuItem(getMenuItem("File"), "View App Data").click() expect(open.opn).to.be.calledWith(appData.path()) it "calls logout callback when Log Out is clicked", -> - onLogOutClicked = @sandbox.stub() + onLogOutClicked = sinon.stub() menu.set({onLogOutClicked}) getSubMenuItem(getMenuItem("File"), "Log Out").click() expect(onLogOutClicked).to.be.called it "calls original logout callback when menu is reset without new callback", -> - onLogOutClicked = @sandbox.stub() + onLogOutClicked = sinon.stub() menu.set({onLogOutClicked}) menu.set() getSubMenuItem(getMenuItem("File"), "Log Out").click() @@ -232,7 +232,7 @@ describe "gui/menu", -> expect(@devSubmenu[0].accelerator).to.equal("CmdOrCtrl+R") it "reloads focused window when Reload is clicked", -> - reload = @sandbox.stub() + reload = sinon.stub() @devSubmenu[0].click(null, {reload}) expect(reload).to.be.called @@ -248,7 +248,7 @@ describe "gui/menu", -> expect(getMenuItem("Developer Tools").submenu[1].accelerator).to.equal("Ctrl+Shift+I") it "toggles dev tools on focused window when Toggle Developer Tools is clicked", -> - toggleDevTools = @sandbox.stub() + toggleDevTools = sinon.stub() @devSubmenu[1].click(null, {toggleDevTools}) expect(toggleDevTools).to.be.called diff --git a/packages/server/test/unit/gui/project_spec.coffee b/packages/server/test/unit/gui/project_spec.coffee index daf1efc9b4a9..ba5453cceb27 100644 --- a/packages/server/test/unit/gui/project_spec.coffee +++ b/packages/server/test/unit/gui/project_spec.coffee @@ -22,7 +22,7 @@ # context ".open", -> # beforeEach -> # @projectInstance = { -# getConfig: @sandbox.stub().resolves({proxyUrl: "foo", socketIoRoute: "bar"}) +# getConfig: sinon.stub().resolves({proxyUrl: "foo", socketIoRoute: "bar"}) # } # # browsers = [{ @@ -31,9 +31,9 @@ # path: "/path/to/Chrome.app" # majorVersion: "2077" # }] -# @sandbox.stub(launcher, "getBrowsers").resolves(browsers) -# @sandbox.stub(extension, "setHostAndPath").withArgs("foo", "bar").resolves() -# @open = @sandbox.stub(Project.prototype, "open").resolves(@projectInstance) +# sinon.stub(launcher, "getBrowsers").resolves(browsers) +# sinon.stub(extension, "setHostAndPath").withArgs("foo", "bar").resolves() +# @open = sinon.stub(Project.prototype, "open").resolves(@projectInstance) # # it "resolves with opened project instance", -> # project.open(@todosPath) @@ -55,7 +55,7 @@ # expect(@open.getCall(0).args[0].onReloadBrowser).to.be.a("function") # # it "passes onReloadBrowser which calls relaunch with url + browser", -> -# relaunch = @sandbox.stub(project, "relaunch") +# relaunch = sinon.stub(project, "relaunch") # # project.open(@todosPath) # .then => diff --git a/packages/server/test/unit/gui/windows_spec.coffee b/packages/server/test/unit/gui/windows_spec.coffee index caaa638d85d8..ab1ed11fe1fe 100644 --- a/packages/server/test/unit/gui/windows_spec.coffee +++ b/packages/server/test/unit/gui/windows_spec.coffee @@ -15,23 +15,23 @@ describe "lib/gui/windows", -> Windows.reset() @win = new EE() - @win.loadURL = @sandbox.stub() - @win.destroy = @sandbox.stub() - @win.getSize = @sandbox.stub().returns([1, 2]) - @win.getPosition = @sandbox.stub().returns([3, 4]) + @win.loadURL = sinon.stub() + @win.destroy = sinon.stub() + @win.getSize = sinon.stub().returns([1, 2]) + @win.getPosition = sinon.stub().returns([3, 4]) @win.webContents = new EE() - @win.webContents.openDevTools = @sandbox.stub() - @win.isDestroyed = @sandbox.stub().returns(false) + @win.webContents.openDevTools = sinon.stub() + @win.isDestroyed = sinon.stub().returns(false) - @sandbox.stub(Windows, "_newBrowserWindow").returns(@win) + sinon.stub(Windows, "_newBrowserWindow").returns(@win) afterEach -> Windows.reset() context ".getBrowserAutomation", -> beforeEach -> - @sandbox.stub(Windows, "automation") - @sandbox.stub(Windows, "getByWebContents") + sinon.stub(Windows, "automation") + sinon.stub(Windows, "getByWebContents") it "gets window and passes to electron.automation", -> Windows.getByWebContents.withArgs("foo").returns("bar") @@ -41,7 +41,7 @@ describe "lib/gui/windows", -> context ".getByWebContents", -> beforeEach -> - @sandbox.stub(BrowserWindow, "fromWebContents") + sinon.stub(BrowserWindow, "fromWebContents") it "calls BrowserWindow.fromWebContents", -> BrowserWindow.fromWebContents.withArgs("foo").returns("bar") @@ -49,7 +49,7 @@ describe "lib/gui/windows", -> context ".open", -> beforeEach -> - @sandbox.stub(Windows, "create").returns(@win) + sinon.stub(Windows, "create").returns(@win) it "sets default options", -> options = { @@ -79,9 +79,9 @@ describe "lib/gui/windows", -> url = "https://github.com/login" url2 = "https://github.com?code=code123" - @sandbox.stub(user, "getLoginUrl").resolves(url) + sinon.stub(user, "getLoginUrl").resolves(url) - @sandbox.stub(@win.webContents, "on").withArgs("will-navigate").yieldsAsync({}, url2) + sinon.stub(@win.webContents, "on").withArgs("will-navigate").yieldsAsync({}, url2) Windows.open("/path/to/project", options) .then (code) => @@ -97,9 +97,9 @@ describe "lib/gui/windows", -> url = "https://github.com/login" url2 = "https://github.com?code=code123" - @sandbox.stub(user, "getLoginUrl").resolves(url) + sinon.stub(user, "getLoginUrl").resolves(url) - @sandbox.stub(@win.webContents, "on").withArgs("did-get-redirect-request").yieldsAsync({}, "foo", url2) + sinon.stub(@win.webContents, "on").withArgs("did-get-redirect-request").yieldsAsync({}, "foo", url2) Windows.open("/path/to/project", options) .then (code) => @@ -121,9 +121,9 @@ describe "lib/gui/windows", -> beforeEach -> savedState() .then (@state) => - @sandbox.stub(@state, "set") + sinon.stub(@state, "set") - @projectPath = undefined + @projectRoot = undefined @keys = { width: "theWidth" height: "someHeight" @@ -135,9 +135,9 @@ describe "lib/gui/windows", -> it "saves size and position when window resizes, debounced", -> ## tried using useFakeTimers here, but it didn't work for some ## reason, so this is the next best thing - @sandbox.stub(_, "debounce").returnsArg(0) + sinon.stub(_, "debounce").returnsArg(0) - Windows.trackState(@projectPath, @win, @keys) + Windows.trackState(@projectRoot, @win, @keys) @win.emit("resize") expect(_.debounce).to.be.called @@ -155,7 +155,7 @@ describe "lib/gui/windows", -> it "returns if window isDestroyed on resize", -> @win.isDestroyed.returns(true) - Windows.trackState(@projectPath, @win, @keys) + Windows.trackState(@projectRoot, @win, @keys) @win.emit("resize") Promise @@ -166,8 +166,8 @@ describe "lib/gui/windows", -> it "saves position when window moves, debounced", -> ## tried using useFakeTimers here, but it didn't work for some ## reason, so this is the next best thing - @sandbox.stub(_, "debounce").returnsArg(0) - Windows.trackState(@projectPath, @win, @keys) + sinon.stub(_, "debounce").returnsArg(0) + Windows.trackState(@projectRoot, @win, @keys) @win.emit("moved") Promise @@ -181,7 +181,7 @@ describe "lib/gui/windows", -> it "returns if window isDestroyed on moved", -> @win.isDestroyed.returns(true) - Windows.trackState(@projectPath, @win, @keys) + Windows.trackState(@projectRoot, @win, @keys) @win.emit("moved") Promise @@ -190,7 +190,7 @@ describe "lib/gui/windows", -> expect(@state.set).not.to.be.called it "saves dev tools state when opened", -> - Windows.trackState(@projectPath, @win, @keys) + Windows.trackState(@projectRoot, @win, @keys) @win.webContents.emit("devtools-opened") Promise @@ -199,7 +199,7 @@ describe "lib/gui/windows", -> expect(@state.set).to.be.calledWith({whatsUpWithDevTools: true}) it "saves dev tools state when closed", -> - Windows.trackState(@projectPath, @win, @keys) + Windows.trackState(@projectRoot, @win, @keys) @win.webContents.emit("devtools-closed") Promise @@ -210,9 +210,9 @@ describe "lib/gui/windows", -> context ".automation", -> beforeEach -> @cookies = { - set: @sandbox.stub() - get: @sandbox.stub() - remove: @sandbox.stub() + set: sinon.stub() + get: sinon.stub() + remove: sinon.stub() } @win = { diff --git a/packages/server/test/unit/human_time_spec.coffee b/packages/server/test/unit/human_time_spec.coffee index 977a1dc1e486..cc095f6147c0 100644 --- a/packages/server/test/unit/human_time_spec.coffee +++ b/packages/server/test/unit/human_time_spec.coffee @@ -4,11 +4,25 @@ humanInterval = require("human-interval") humanTime = require("#{root}lib/util/human_time") describe "lib/util/human_time", -> - it "outputs minutes + seconds", -> - expect(humanTime(humanInterval("2 minutes and 3 seconds"))).to.eq("2 minutes, 3 seconds") - expect(humanTime(humanInterval("65 minutes"))).to.eq("65 minutes, 0 seconds") - expect(humanTime(humanInterval("1 minute"))).to.eq("1 minute, 0 seconds") - - it "outputs seconds", -> - expect(humanTime(humanInterval("59 seconds"))).to.eq("59 seconds") - expect(humanTime(humanInterval("1 second"))).to.eq("1 second") + context ".long", -> + it "outputs minutes + seconds", -> + expect(humanTime.long(humanInterval("2 minutes and 3 seconds"))).to.eq("2 minutes, 3 seconds") + expect(humanTime.long(humanInterval("65 minutes"))).to.eq("65 minutes, 0 seconds") + expect(humanTime.long(humanInterval("1 minute"))).to.eq("1 minute, 0 seconds") + + it "outputs seconds", -> + expect(humanTime.long(humanInterval("59 seconds"))).to.eq("59 seconds") + expect(humanTime.long(humanInterval("1 second"))).to.eq("1 second") + + context ".short", -> + it "outputs mins", -> + expect(humanTime.short(humanInterval("2 minutes and 3 seconds"))).to.eq("2m, 3s") + expect(humanTime.short(humanInterval("65 minutes"))).to.eq("65m") + expect(humanTime.short(humanInterval("1 minute"))).to.eq("1m") + + it "outputs seconds", -> + expect(humanTime.short(humanInterval("59 seconds"))).to.eq("59s") + expect(humanTime.short(humanInterval("1 second"))).to.eq("1s") + expect(humanTime.short(0)).to.eq("0s") + expect(humanTime.short(500)).to.eq("500ms") + expect(humanTime.short(10)).to.eq("10ms") diff --git a/packages/server/test/unit/ids_spec.coffee b/packages/server/test/unit/ids_spec.coffee deleted file mode 100644 index cf411f3e3ac5..000000000000 --- a/packages/server/test/unit/ids_spec.coffee +++ /dev/null @@ -1,53 +0,0 @@ -require("../spec_helper") - -path = require("path") -Fixtures = require("../support/helpers/fixtures") -ids = require("#{root}lib/ids") - -describe "lib/ids", -> - beforeEach -> - Fixtures.scaffold() - - @testIdsPath = path.join(Fixtures.projectPath("ids"), "cypress", "integration") - - afterEach -> - Fixtures.remove() - - context ".get", -> - it "returns an array of ids", -> - ids.get(@testIdsPath) - .then (array) -> - expect(array).to.include(" [000]", " [001]", " [002]", "[i9w]", "[abc]") - - it "returns stats", -> - ids.remove(@testIdsPath) - .then (stats) -> - expect(stats).to.deep.eq({ - ids: 5 - files: 2 - }) - - it "removes ids", -> - ids - .get(@testIdsPath) - .then (array) => - expect(array.length).to.be.gt(0) - - ids.remove(@testIdsPath) - .then => - ids.get(@testIdsPath) - .then (array) -> - expect(array.length).to.eq(0) - - it "strips out whitespace next to the id", -> - foo = path.join(@testIdsPath, "foo.coffee") - - ids.remove(@testIdsPath) - .then => - fs.readFileAsync(foo, "utf8").then (contents) -> - expect(contents).to.eq """ - describe "foo", -> - it "bars", -> - - it 'quux' - """ diff --git a/packages/server/test/unit/logger_spec.coffee b/packages/server/test/unit/logger_spec.coffee index 6d36fc215ac0..1a7ad3471175 100644 --- a/packages/server/test/unit/logger_spec.coffee +++ b/packages/server/test/unit/logger_spec.coffee @@ -80,7 +80,7 @@ describe "lib/logger", -> describe "#exitOnError", -> it "invokes logger.defaultErrorHandler", -> err = new Error() - defaultErrorHandler = @sandbox.stub(logger, "defaultErrorHandler") + defaultErrorHandler = sinon.stub(logger, "defaultErrorHandler") logger.exitOnError(err) expect(defaultErrorHandler).to.be.calledWith err @@ -89,8 +89,8 @@ describe "lib/logger", -> logger.unsetSettings() @err = new Error() - @exit = @sandbox.stub(process, "exit") - @create = @sandbox.stub(exception, "create").resolves() + @exit = sinon.stub(process, "exit") + @create = sinon.stub(exception, "create").resolves() afterEach -> logger.unsetSettings() @@ -125,7 +125,7 @@ describe "lib/logger", -> expect(@exit).to.be.calledWith(1) it "calls Log#errorhandler", -> - fn = @sandbox.spy() + fn = sinon.spy() logger.setErrorHandler(fn) logger.defaultErrorHandler(@err) Promise.delay(50).then => @@ -139,7 +139,7 @@ describe "lib/logger", -> describe "unhandledRejection", -> it "passes error to defaultErrorHandler", -> - defaultErrorHandler = @sandbox.stub(logger, "defaultErrorHandler") + defaultErrorHandler = sinon.stub(logger, "defaultErrorHandler") handlers = process.listeners("unhandledRejection") @@ -150,7 +150,7 @@ describe "lib/logger", -> handlers[0](err) it "catches unhandled rejections", -> - defaultErrorHandler = @sandbox.stub(logger, "defaultErrorHandler") + defaultErrorHandler = sinon.stub(logger, "defaultErrorHandler") Promise .resolve("") diff --git a/packages/server/test/unit/modes/headless_spec.coffee b/packages/server/test/unit/modes/headless_spec.coffee deleted file mode 100644 index 344bf537e846..000000000000 --- a/packages/server/test/unit/modes/headless_spec.coffee +++ /dev/null @@ -1,493 +0,0 @@ -require("../../spec_helper") - -random = require("randomstring") -Promise = require("bluebird") -electron = require("electron") -user = require("#{root}../lib/user") -video = require("#{root}../lib/video") -errors = require("#{root}../lib/errors") -Project = require("#{root}../lib/project") -Reporter = require("#{root}../lib/reporter") -headless = require("#{root}../lib/modes/headless") -openProject = require("#{root}../lib/open_project") - -describe "lib/modes/headless", -> - beforeEach -> - @projectInstance = Project("/_test-output/path/to/project") - - context ".getId", -> - it "returns random.generate string", -> - @sandbox.spy(random, "generate") - - id = headless.getId() - expect(id.length).to.eq(5) - - expect(random.generate).to.be.calledWith({ - length: 5 - capitalization: "lowercase" - }) - - context ".openProject", -> - beforeEach -> - @sandbox.stub(openProject, "create").resolves() - - options = { - port: 8080 - env: {foo: "bar"} - projectPath: "/_test-output/path/to/project/foo" - } - - headless.openProject(1234, options) - - it "calls openProject.create with projectPath + options", -> - expect(openProject.create).to.be.calledWithMatch("/_test-output/path/to/project/foo", { - port: 8080 - projectPath: "/_test-output/path/to/project/foo" - env: {foo: "bar"} - }, { - morgan: false - socketId: 1234 - report: true - isTextTerminal: true - }) - - it "emits 'exitEarlyWithErr' with error message onError", -> - @sandbox.stub(openProject, "emit") - expect(openProject.create.lastCall.args[2].onError).to.be.a("function") - openProject.create.lastCall.args[2].onError({ message: "the message" }) - expect(openProject.emit).to.be.calledWith("exitEarlyWithErr", "the message") - - context ".getElectronProps", -> - it "sets width and height", -> - props = headless.getElectronProps() - - expect(props.width).to.eq(1280) - expect(props.height).to.eq(720) - - it "sets show to boolean", -> - props = headless.getElectronProps(false) - expect(props.show).to.be.false - - props = headless.getElectronProps(true) - expect(props.show).to.be.true - - it "sets recordFrameRate and onPaint when write is true", -> - write = @sandbox.stub() - - image = { - toJPEG: @sandbox.stub().returns("imgdata") - } - - props = headless.getElectronProps(true, {}, write) - - expect(props.recordFrameRate).to.eq(20) - - props.onPaint({}, false, image) - - expect(write).to.be.calledWith("imgdata") - - it "does not set recordFrameRate or onPaint when write is falsy", -> - props = headless.getElectronProps(true, {}, false) - - expect(props).not.to.have.property("recordFrameRate") - expect(props).not.to.have.property("onPaint") - - it "sets options.show = false onNewWindow callback", -> - options = {show: true} - - props = headless.getElectronProps() - props.onNewWindow(null, null, null, null, options) - - expect(options.show).to.eq(false) - - it "emits exitEarlyWithErr when webContents crashed", -> - @sandbox.spy(errors, "get") - @sandbox.spy(errors, "log") - - emit = @sandbox.stub(@projectInstance, "emit") - - props = headless.getElectronProps(true, @projectInstance) - - props.onCrashed() - - expect(errors.get).to.be.calledWith("RENDERER_CRASHED") - expect(errors.log).to.be.calledOnce - expect(emit).to.be.calledWithMatch("exitEarlyWithErr", "We detected that the Chromium Renderer process just crashed.") - - context ".launchBrowser", -> - beforeEach -> - @launch = @sandbox.stub(openProject, "launch") - - @sandbox.stub(headless, "getElectronProps").returns({foo: "bar"}) - - @sandbox.stub(headless, "screenshotMetadata").returns({a: "a"}) - - it "gets electron props by default", -> - screenshots = [] - - headless.launchBrowser({ - browser: undefined - spec: null - project: @projectInstance - write: "write" - gui: null - screenshots: screenshots - }) - - expect(headless.getElectronProps).to.be.calledWith(false, @projectInstance, "write") - - expect(@launch).to.be.calledWithMatch("electron", null, {foo: "bar"}) - - browserOpts = @launch.firstCall.args[2] - - onAfterResponse = browserOpts.automationMiddleware.onAfterResponse - - expect(onAfterResponse).to.be.a("function") - - onAfterResponse("take:screenshot") - onAfterResponse("get:cookies") - - expect(screenshots).to.deep.eq([{a: "a"}]) - - it "can launch chrome", -> - headless.launchBrowser({ - browser: "chrome" - spec: "spec" - }) - - expect(headless.getElectronProps).not.to.be.called - - expect(@launch).to.be.calledWithMatch("chrome", "spec", {}) - - context ".postProcessRecording", -> - beforeEach -> - @sandbox.stub(video, "process").resolves() - - it "calls video process with name, cname and videoCompression", -> - end = -> Promise.resolve() - - headless.postProcessRecording(end, "foo", "foo-compress", 32, true) - .then -> - expect(video.process).to.be.calledWith("foo", "foo-compress", 32) - - it "does not call video process when videoCompression is false", -> - end = -> Promise.resolve() - - headless.postProcessRecording(end, "foo", "foo-compress", false, true) - .then -> - expect(video.process).not.to.be.called - - it "calls video process if we have been told to upload videos", -> - end = -> Promise.resolve() - - headless.postProcessRecording(end, "foo", "foo-compress", 32, true) - .then -> - expect(video.process).to.be.calledWith("foo", "foo-compress", 32) - - it "does not call video process if there are no failing tests and we have set not to upload video on passing", -> - end = -> Promise.resolve() - - headless.postProcessRecording(end, "foo", "foo-compress", 32, false) - .then -> - expect(video.process).not.to.be.called - - context ".waitForBrowserToConnect", -> - it "throws TESTS_DID_NOT_START_FAILED after 3 connection attempts", -> - @sandbox.spy(errors, "warning") - @sandbox.spy(errors, "get") - @sandbox.spy(openProject, "closeBrowser") - @sandbox.stub(headless, "launchBrowser").resolves() - @sandbox.stub(headless, "waitForSocketConnection").resolves(Promise.delay(1000)) - emit = @sandbox.stub(@projectInstance, "emit") - - headless.waitForBrowserToConnect({project: @projectInstance, timeout: 10}) - .then -> - expect(openProject.closeBrowser).to.be.calledThrice - expect(headless.launchBrowser).to.be.calledThrice - expect(errors.warning).to.be.calledWith("TESTS_DID_NOT_START_RETRYING", "Retrying...") - expect(errors.warning).to.be.calledWith("TESTS_DID_NOT_START_RETRYING", "Retrying again...") - expect(errors.get).to.be.calledWith("TESTS_DID_NOT_START_FAILED") - expect(emit).to.be.calledWith("exitEarlyWithErr", "The browser never connected. Something is wrong. The tests cannot run. Aborting...") - - context ".waitForSocketConnection", -> - beforeEach -> - @projectStub = @sandbox.stub({ - on: -> - removeListener: -> - }) - - it "attaches fn to 'socket:connected' event", -> - headless.waitForSocketConnection(@projectStub, 1234) - expect(@projectStub.on).to.be.calledWith("socket:connected") - - it "calls removeListener if socketId matches id", -> - @projectStub.on.yields(1234) - - headless.waitForSocketConnection(@projectStub, 1234) - .then => - expect(@projectStub.removeListener).to.be.calledWith("socket:connected") - - describe "integration", -> - it "resolves undefined when socket:connected fires", -> - process.nextTick => - @projectInstance.emit("socket:connected", 1234) - - headless.waitForSocketConnection(@projectInstance, 1234) - .then (ret) -> - expect(ret).to.be.undefined - - it "does not resolve if socketId does not match id", -> - process.nextTick => - @projectInstance.emit("socket:connected", 12345) - - headless - .waitForSocketConnection(@projectInstance, 1234) - .timeout(50) - .then -> - throw new Error("should time out and not resolve") - .catch Promise.TimeoutError, (err) -> - - it "actually removes the listener", -> - process.nextTick => - @projectInstance.emit("socket:connected", 12345) - expect(@projectInstance.listeners("socket:connected")).to.have.length(1) - @projectInstance.emit("socket:connected", "1234") - expect(@projectInstance.listeners("socket:connected")).to.have.length(1) - @projectInstance.emit("socket:connected", 1234) - expect(@projectInstance.listeners("socket:connected")).to.have.length(0) - - headless.waitForSocketConnection(@projectInstance, 1234) - - context ".waitForTestsToFinishRunning", -> - beforeEach -> - @sandbox.stub(@projectInstance, "getConfig").resolves({}) - - it "end event resolves with obj, displays stats, displays screenshots, setsFailingTests", -> - started = new Date - screenshots = [{}, {}, {}] - end = -> - stats = { - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - failingTests: [4,5,6] - } - - @sandbox.stub(Reporter, "setVideoTimestamp").withArgs(started, stats.failingTests).returns([1,2,3]) - @sandbox.stub(headless, "postProcessRecording").resolves() - @sandbox.spy(headless, "displayStats") - @sandbox.spy(headless, "displayScreenshots") - - process.nextTick => - @projectInstance.emit("end", stats) - - headless.waitForTestsToFinishRunning({ - project: @projectInstance, - name: "foo.mp4" - cname: "foo-compressed.mp4" - videoCompression: 32 - videoUploadOnPasses: true - gui: false - screenshots - started - end - }) - .then (obj) -> - expect(headless.postProcessRecording).to.be.calledWith(end, "foo.mp4", "foo-compressed.mp4", 32, true) - - results = headless.collectTestResults(obj) - expect(headless.displayStats).to.be.calledWith(results) - expect(headless.displayScreenshots).to.be.calledWith(screenshots) - - expect(obj).to.deep.eq({ - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - config: {} - failingTests: [1,2,3] - screenshots: screenshots - video: "foo.mp4" - shouldUploadVideo: true - }) - - it "exitEarlyWithErr event resolves with no tests, error, and empty failingTests", -> - err = new Error("foo") - started = new Date - screenshots = [{}, {}, {}] - end = -> - - @sandbox.stub(headless, "postProcessRecording").resolves() - @sandbox.spy(headless, "displayStats") - @sandbox.spy(headless, "displayScreenshots") - - process.nextTick => - expect(@projectInstance.listeners("exitEarlyWithErr")).to.have.length(1) - @projectInstance.emit("exitEarlyWithErr", err.message) - expect(@projectInstance.listeners("exitEarlyWithErr")).to.have.length(0) - - headless.waitForTestsToFinishRunning({ - project: @projectInstance, - name: "foo.mp4" - cname: "foo-compressed.mp4" - videoCompression: 32 - videoUploadOnPasses: true - gui: false - screenshots - started - end - }) - .then (obj) -> - expect(headless.postProcessRecording).to.be.calledWith(end, "foo.mp4", "foo-compressed.mp4", 32, true) - - results = headless.collectTestResults(obj) - expect(headless.displayStats).to.be.calledWith(results) - expect(headless.displayScreenshots).to.be.calledWith(screenshots) - - expect(obj).to.deep.eq({ - error: err.message - failures: 1 - tests: 0 - passes: 0 - pending: 0 - duration: 0 - config: {} - failingTests: [] - screenshots: screenshots - video: "foo.mp4" - shouldUploadVideo: true - }) - - it "should not upload video when videoUploadOnPasses is false and no failing tests", -> - process.nextTick => - @projectInstance.emit("end", { - failingTests: [] - }) - - @sandbox.spy(headless, "postProcessRecording") - @sandbox.spy(video, "process") - end = @sandbox.stub().resolves() - - headless.waitForTestsToFinishRunning({ - project: @projectInstance, - name: "foo.mp4" - cname: "foo-compressed.mp4" - videoCompression: 32 - videoUploadOnPasses: false - gui: false - end - }) - .then (obj) -> - expect(headless.postProcessRecording).to.be.calledWith(end, "foo.mp4", "foo-compressed.mp4", 32, false) - - expect(video.process).not.to.be.called - - context ".listenForProjectEnd", -> - it "resolves with end event + argument", -> - process.nextTick => - @projectInstance.emit("end", {foo: "bar"}) - - headless.listenForProjectEnd(@projectInstance) - .then (obj) -> - expect(obj).to.deep.eq({ - foo: "bar" - }) - - it "stops listening to end event", -> - process.nextTick => - expect(@projectInstance.listeners("end")).to.have.length(1) - @projectInstance.emit("end", {foo: "bar"}) - expect(@projectInstance.listeners("end")).to.have.length(0) - - headless.listenForProjectEnd(@projectInstance) - - context ".run browser vs video recording", -> - beforeEach -> - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() - @sandbox.stub(user, "ensureAuthToken") - @sandbox.stub(Project, "ensureExists").resolves() - @sandbox.stub(headless, "getId").returns(1234) - @sandbox.stub(headless, "openProject").resolves(openProject) - @sandbox.stub(headless, "waitForSocketConnection").resolves() - @sandbox.stub(headless, "waitForTestsToFinishRunning").resolves({failures: 10}) - @sandbox.spy(headless, "waitForBrowserToConnect") - @sandbox.stub(openProject, "launch").resolves() - @sandbox.stub(openProject, "getProject").resolves(@projectInstance) - @sandbox.spy(errors, "warning") - @sandbox.stub(@projectInstance, "getConfig").resolves({ - proxyUrl: "http://localhost:12345", - videoRecording: true, - videosFolder: "videos" - }) - - it "shows no warnings for default browser", -> - headless.run() - .then -> - expect(errors.warning).to.not.be.called - - it "shows no warnings for electron browser", -> - headless.run({browser: "electron"}) - .then -> - expect(errors.warning).to.not.be.calledWith("CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER") - - it "disables video recording on headed runs", -> - headless.run({headed: true}) - .then -> - expect(errors.warning).to.be.calledWith("CANNOT_RECORD_VIDEO_HEADED") - - it "disables video recording for non-electron browser", -> - headless.run({browser: "chrome"}) - .then -> - expect(errors.warning).to.be.calledWith("CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER") - - context ".run", -> - beforeEach -> - @sandbox.stub(@projectInstance, "getConfig").resolves({proxyUrl: "http://localhost:12345"}) - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() - @sandbox.stub(user, "ensureAuthToken") - @sandbox.stub(Project, "ensureExists").resolves() - @sandbox.stub(headless, "getId").returns(1234) - @sandbox.stub(headless, "openProject").resolves(openProject) - @sandbox.stub(headless, "waitForSocketConnection").resolves() - @sandbox.stub(headless, "waitForTestsToFinishRunning").resolves({failures: 10}) - @sandbox.spy(headless, "waitForBrowserToConnect") - @sandbox.stub(openProject, "launch").resolves() - @sandbox.stub(openProject, "getProject").resolves(@projectInstance) - - it "no longer ensures user session", -> - headless.run() - .then -> - expect(user.ensureAuthToken).not.to.be.called - - it "returns stats", -> - headless.run() - .then (stats) -> - expect(stats).to.deep.eq({failures: 10}) - - it "passes id + options to openProject", -> - headless.run({foo: "bar"}) - .then -> - expect(headless.openProject).to.be.calledWithMatch(1234, {foo: "bar"}) - - it "passes project + id to waitForBrowserToConnect", -> - headless.run() - .then => - expect(headless.waitForBrowserToConnect).to.be.calledWithMatch({ - project: @projectInstance - id: 1234 - }) - - it "passes project to waitForTestsToFinishRunning", -> - headless.run() - .then => - expect(headless.waitForTestsToFinishRunning).to.be.calledWithMatch({ - project: @projectInstance - }) - - it "passes headed to openProject.launch", -> - headless.run({headed: true}) - .then -> - expect(openProject.launch).to.be.calledWithMatch("electron", undefined, {show: true}) diff --git a/packages/server/test/unit/modes/headed_spec.coffee b/packages/server/test/unit/modes/interactive_spec.coffee similarity index 63% rename from packages/server/test/unit/modes/headed_spec.coffee rename to packages/server/test/unit/modes/interactive_spec.coffee index ca16acdfc1da..aa1a5a8b7730 100644 --- a/packages/server/test/unit/modes/headed_spec.coffee +++ b/packages/server/test/unit/modes/interactive_spec.coffee @@ -7,31 +7,31 @@ user = require("#{root}../lib/user") logger = require("#{root}../lib/logger") Updater = require("#{root}../lib/updater") savedState = require("#{root}../lib/saved_state") -headed = require("#{root}../lib/modes/headed") menu = require("#{root}../lib/gui/menu") Events = require("#{root}../lib/gui/events") Windows = require("#{root}../lib/gui/windows") +interactiveMode = require("#{root}../lib/modes/interactive") -describe "gui/headed", -> +describe "gui/interactive", -> context ".isMac", -> it "returns true if os.platform is darwin", -> - @sandbox.stub(os, "platform").returns("darwin") + sinon.stub(os, "platform").returns("darwin") - expect(headed.isMac()).to.be.true + expect(interactiveMode.isMac()).to.be.true it "returns false if os.platform isnt darwin", -> - @sandbox.stub(os, "platform").returns("linux64") + sinon.stub(os, "platform").returns("linux64") - expect(headed.isMac()).to.be.false + expect(interactiveMode.isMac()).to.be.false context ".getWindowArgs", -> it "exits process when onClose is called", -> - @sandbox.stub(process, "exit") - headed.getWindowArgs({}).onClose() + sinon.stub(process, "exit") + interactiveMode.getWindowArgs({}).onClose() expect(process.exit).to.be.called it "tracks state properties", -> - trackState = headed.getWindowArgs({}).trackState + trackState = interactiveMode.getWindowArgs({}).trackState args = _.pick(trackState, "width", "height", "x", "y", "devTools") @@ -44,44 +44,44 @@ describe "gui/headed", -> }) it "renders with saved width if it exists", -> - expect(headed.getWindowArgs({appWidth: 1}).width).to.equal(1) + expect(interactiveMode.getWindowArgs({appWidth: 1}).width).to.equal(1) it "renders with default width if no width saved", -> - expect(headed.getWindowArgs({}).width).to.equal(800) + expect(interactiveMode.getWindowArgs({}).width).to.equal(800) it "renders with saved height if it exists", -> - expect(headed.getWindowArgs({appHeight: 2}).height).to.equal(2) + expect(interactiveMode.getWindowArgs({appHeight: 2}).height).to.equal(2) it "renders with default height if no height saved", -> - expect(headed.getWindowArgs({}).height).to.equal(550) + expect(interactiveMode.getWindowArgs({}).height).to.equal(550) it "renders with saved x if it exists", -> - expect(headed.getWindowArgs({appX: 3}).x).to.equal(3) + expect(interactiveMode.getWindowArgs({appX: 3}).x).to.equal(3) it "renders with no x if no x saved", -> - expect(headed.getWindowArgs({}).x).to.be.undefined + expect(interactiveMode.getWindowArgs({}).x).to.be.undefined it "renders with saved y if it exists", -> - expect(headed.getWindowArgs({appY: 4}).y).to.equal(4) + expect(interactiveMode.getWindowArgs({appY: 4}).y).to.equal(4) it "renders with no y if no y saved", -> - expect(headed.getWindowArgs({}).y).to.be.undefined + expect(interactiveMode.getWindowArgs({}).y).to.be.undefined describe "on window focus", -> beforeEach -> - @sandbox.stub(menu, "set") + sinon.stub(menu, "set") it "calls menu.set withDevTools: true when in dev env", -> env = process.env["CYPRESS_ENV"] process.env["CYPRESS_ENV"] = "development" - headed.getWindowArgs({}).onFocus() + interactiveMode.getWindowArgs({}).onFocus() expect(menu.set.lastCall.args[0].withDevTools).to.be.true process.env["CYPRESS_ENV"] = env it "calls menu.set withDevTools: false when not in dev env", -> env = process.env["CYPRESS_ENV"] process.env["CYPRESS_ENV"] = "production" - headed.getWindowArgs({}).onFocus() + interactiveMode.getWindowArgs({}).onFocus() expect(menu.set.lastCall.args[0].withDevTools).to.be.false process.env["CYPRESS_ENV"] = env @@ -90,52 +90,52 @@ describe "gui/headed", -> @win = {} @state = {} - @sandbox.stub(menu, "set") - @sandbox.stub(Events, "start") - @sandbox.stub(Windows, "open").resolves(@win) - @sandbox.stub(Windows, "trackState") + sinon.stub(menu, "set") + sinon.stub(Events, "start") + sinon.stub(Windows, "open").resolves(@win) + sinon.stub(Windows, "trackState") state = savedState() - @sandbox.stub(state, "get").resolves(@state) + sinon.stub(state, "get").resolves(@state) it "calls Events.start with options, adding env, onFocusTests, and os", -> - @sandbox.stub(os, "platform").returns("someOs") + sinon.stub(os, "platform").returns("someOs") opts = {} - headed.ready(opts).then -> + interactiveMode.ready(opts).then -> expect(Events.start).to.be.called expect(Events.start.lastCall.args[0].onFocusTests).to.be.a("function") expect(Events.start.lastCall.args[0].os).to.equal("someOs") it "calls menu.set", -> - headed.ready({}).then -> + interactiveMode.ready({}).then -> expect(menu.set).to.be.calledOnce it "calls menu.set withDevTools: true when in dev env", -> env = process.env["CYPRESS_ENV"] process.env["CYPRESS_ENV"] = "development" - headed.ready({}).then -> + interactiveMode.ready({}).then -> expect(menu.set.lastCall.args[0].withDevTools).to.be.true process.env["CYPRESS_ENV"] = env it "calls menu.set withDevTools: false when not in dev env", -> env = process.env["CYPRESS_ENV"] process.env["CYPRESS_ENV"] = "production" - headed.ready({}).then -> + interactiveMode.ready({}).then -> expect(menu.set.lastCall.args[0].withDevTools).to.be.false process.env["CYPRESS_ENV"] = env it "resolves with win", -> - headed.ready({}).then (win) => + interactiveMode.ready({}).then (win) => expect(win).to.eq(@win) context ".run", -> beforeEach -> - @sandbox.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() it "calls ready with options", -> - @sandbox.stub(headed, "ready") + sinon.stub(interactiveMode, "ready") opts = {} - headed.run(opts).then -> - expect(headed.ready).to.be.calledWith(opts) + interactiveMode.run(opts).then -> + expect(interactiveMode.ready).to.be.calledWith(opts) diff --git a/packages/server/test/unit/modes/record_spec.coffee b/packages/server/test/unit/modes/record_spec.coffee index c68279a38b34..c72f5e687ee8 100644 --- a/packages/server/test/unit/modes/record_spec.coffee +++ b/packages/server/test/unit/modes/record_spec.coffee @@ -1,28 +1,24 @@ require("../../spec_helper") +_ = require("lodash") os = require("os") -R = require("ramda") +commitInfo = require("@cypress/commit-info") api = require("#{root}../lib/api") -stdout = require("#{root}../lib/stdout") errors = require("#{root}../lib/errors") logger = require("#{root}../lib/logger") upload = require("#{root}../lib/upload") -Project = require("#{root}../lib/project") -terminal = require("#{root}../lib/util/terminal") -record = require("#{root}../lib/modes/record") -headless = require("#{root}../lib/modes/headless") +browsers = require("#{root}../lib/browsers") +recordMode = require("#{root}../lib/modes/record") +system = require("#{root}../lib/util/system") ciProvider = require("#{root}../lib/util/ci_provider") -commitInfo = require("@cypress/commit-info") -snapshot = require("snap-shot-it") -initialEnv = R.clone(process.env) +initialEnv = _.clone(process.env) +## NOTE: the majority of the logic of record_spec is +## tested as an e2e/record_spec describe "lib/modes/record", -> - beforeEach -> - @sandbox.stub(ciProvider, "name").returns("circle") - @sandbox.stub(ciProvider, "params").returns({foo: "bar"}) - @sandbox.stub(ciProvider, "buildNum").returns("build-123") - + ## QUESTION: why are these tests here when + ## this is a module... ? context "commitInfo.getBranch", -> beforeEach -> delete process.env.CIRCLE_BRANCH @@ -31,7 +27,7 @@ describe "lib/modes/record", -> delete process.env.CI_BRANCH afterEach -> - process.env = R.clone(initialEnv) + process.env = initialEnv it "gets branch from process.env.CIRCLE_BRANCH", -> process.env.CIRCLE_BRANCH = "bem/circle" @@ -61,13 +57,21 @@ describe "lib/modes/record", -> commitInfo.getBranch().then (ret) -> expect(ret).to.eq("bem/ci") - it "gets branch from git", -> + it "gets branch from git" # this is tested inside @cypress/commit-info - context ".generateProjectBuildId", -> - projectSpecs = ["spec.js"] + context ".createRunAndRecordSpecs", -> + specs = [ + { path: "path/to/spec/a" }, + { path: "path/to/spec/b" } + ] + beforeEach -> - @sandbox.stub(commitInfo, "commitInfo").resolves({ + sinon.stub(ciProvider, "name").returns("circle") + sinon.stub(ciProvider, "params").returns({foo: "bar"}) + sinon.stub(ciProvider, "buildNum").returns("build-123") + + sinon.stub(commitInfo, "commitInfo").resolves({ branch: "master", author: "brian", email: "brian@cypress.io", @@ -75,466 +79,140 @@ describe "lib/modes/record", -> sha: "sha-123", remote: "https://github.com/foo/bar.git" }) - @sandbox.stub(api, "createRun") - @sandbox.stub(Project, "findSpecs").resolves(projectSpecs) - - it "passes list of found specs", -> - api.createRun.resolves() - record.generateProjectBuildId("id-123", "/_test-output/path/to/project", "project", "key-123").then -> - specs = api.createRun.firstCall.args[0].specs - expect(specs).to.eq(projectSpecs) - - it "calls api.createRun with args", -> - api.createRun.resolves() - - record.generateProjectBuildId("id-123", "/_test-output/path/to/project", "project", "key-123").then -> - snapshot(api.createRun.firstCall.args) - - it "passes groupId", -> - api.createRun.resolves() - - group = true - groupId = "gr123" - record.generateProjectBuildId("id-123", "/_test-output/path/to/project", "project", "key-123", group, groupId).then -> - snapshot(api.createRun.firstCall.args) - - it "warns group flag is missing if only groupId is passed", -> - @sandbox.spy(console, "log") - - api.createRun.resolves() - - groupId = "gr123" - record.generateProjectBuildId("id-123", "/_test-output/path/to/project", "project", "key-123", false, groupId).then -> - msg = "Warning: you passed group-id but no group flag" - expect(console.log).to.have.been.calledWith(msg) - - it "figures out groupId from CI environment variables", -> - @sandbox.stub(ciProvider, "groupId").returns("ci-group-123") - - api.createRun.resolves() - - group = true - record.generateProjectBuildId("id-123", "/_test-output/path/to/project", "project", "key-123", group).then -> - snapshot(api.createRun.firstCall.args) - it "handles status code errors of 401", -> - err = new Error - err.statusCode = 401 - - api.createRun.rejects(err) - - key = "3206e6d9-51b6-4766-b2a5-9d173f5158aa" - - record.generateProjectBuildId("id-123", "path", "project", key) - .then -> - throw new Error("should have failed but did not") - .catch (err) -> - expect(err.type).to.eq("RECORD_KEY_NOT_VALID") - expect(err.message).to.include("Key") - expect(err.message).to.include("3206e...158aa") - expect(err.message).to.include("invalid") - - it "handles status code errors of 404", -> - err = new Error - err.statusCode = 404 - - api.createRun.rejects(err) + sinon.stub(api, "createRun").resolves() + + it "calls api.createRun with the right args", -> + key = "recordKey" + projectId = "pId123" + specPattern = ["spec/pattern1", "spec/pattern2"] + projectRoot = "project/root" + runAllSpecs = sinon.stub() + sys = { + osCpus: 1 + osName: 2 + osMemory: 3 + osVersion: 4 + } + browser = { + displayName: "chrome" + version: "59" + } - record.generateProjectBuildId("id-123", "path", "project", "key-123") + recordMode.createRunAndRecordSpecs({ + key + sys + specs + browser + projectId + projectRoot + specPattern + runAllSpecs + }) .then -> - throw new Error("should have failed but did not") - .catch (err) -> - expect(err.type).to.eq("DASHBOARD_PROJECT_NOT_FOUND") - - it "handles all other errors", -> - err = new Error("foo") - - api.createRun.rejects(err) - - @sandbox.spy(errors, "warning") - @sandbox.spy(logger, "createException") - @sandbox.spy(console, "log") - - ## this should not throw - record.generateProjectBuildId(1,2,3,4) - .then (ret) -> - expect(ret).to.be.null - expect(errors.warning).to.be.calledWith("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - expect(console.log).to.be.calledWithMatch("Warning: We encountered an error talking to our servers.") - expect(console.log).to.be.calledWithMatch("Error: foo") - expect(logger.createException).to.be.calledWith(err) + expect(commitInfo.commitInfo).to.be.calledWith(projectRoot) + expect(api.createRun).to.be.calledWith({ + projectId + recordKey: key + specPattern: "spec/pattern1,spec/pattern2" + specs: ["path/to/spec/a", "path/to/spec/b"] + platform: { + osCpus: 1 + osName: 2 + osMemory: 3 + osVersion: 4 + browserName: "chrome" + browserVersion: "59" + } + ci: { + params: {foo: "bar"} + provider: "circle" + buildNumber: "build-123" + } + commit: { + sha: "sha-123", + branch: "master", + authorName: "brian", + authorEmail: "brian@cypress.io", + message: "such hax", + remoteOrigin: "https://github.com/foo/bar.git" + } + }) - context ".uploadStdout", -> + context ".updateInstanceStdout", -> beforeEach -> - @sandbox.stub(api, "updateInstanceStdout") + sinon.stub(api, "updateInstanceStdout") it "calls api.updateInstanceStdout", -> api.updateInstanceStdout.resolves() - record.uploadStdout("id-123", "foobarbaz\n") - - expect(api.updateInstanceStdout).to.be.calledWith({ + options = { instanceId: "id-123" - stdout: "foobarbaz\n" - }) - - it "logs warning on error", -> - err = new Error("foo") - - @sandbox.spy(errors, "warning") - @sandbox.spy(logger, "createException") - @sandbox.spy(console, "log") - - api.updateInstanceStdout.rejects(err) + captured: { toString: -> "foobarbaz\n" } + } - record.uploadStdout("id-123", "asdf") + recordMode.updateInstanceStdout(options) .then -> - expect(errors.warning).to.be.calledWith("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - expect(console.log).to.be.calledWithMatch("This run will not be recorded.") - expect(console.log).to.be.calledWithMatch("Error: foo") - expect(logger.createException).to.be.calledWith(err) + expect(api.updateInstanceStdout).to.be.calledWith({ + instanceId: "id-123" + stdout: "foobarbaz\n" + }) it "does not createException when statusCode is 503", -> err = new Error("foo") err.statusCode = 503 - @sandbox.spy(logger, "createException") + sinon.spy(logger, "createException") api.updateInstanceStdout.rejects(err) - record.uploadStdout("id-123", "Asdfasd") - .then -> - expect(logger.createException).not.to.be.called - - context ".uploadAssets", -> - beforeEach -> - @sandbox.stub(api, "updateInstance") - - it "calls api.updateInstance", -> - api.updateInstance.resolves() - - record.uploadAssets("id-123", { - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - video: "path/to/video" - error: "err msg" - screenshots: [{ - name: "foo" - path: "path/to/screenshot" - }] - failingTests: ["foo"] - config: {foo: "bar"} - }, "foobarbaz") - - expect(api.updateInstance).to.be.calledWith({ + options = { instanceId: "id-123" - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - error: "err msg" - video: true - screenshots: [{name: "foo"}] - failingTests: ["foo"] - cypressConfig: {foo: "bar"} - ciProvider: "circle" - stdout: "foobarbaz" - }) - - it "calls record.upload on success", -> - resp = { - videoUploadUrl: "https://s3.upload.video" - screenshotUploadUrls: [ - { clientId: 1, uploadUrl: "https://s3.upload.screenshot/1"} - { clientId: 2, uploadUrl: "https://s3.upload.screenshot/2"} - ] + captured: { toString: -> "foobarbaz\n" } } - api.updateInstance.resolves(resp) - - @sandbox.stub(upload, "send").resolves() - - record.uploadAssets("id-123", { - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - video: "path/to/video" - screenshots: [{ - clientId: 1 - name: "foo" - path: "path/to/screenshot1" - }, { - clientId: 2 - name: "bar" - path: "path/to/screenshot2" - }] - failingTests: ["foo"] - config: {foo: "bar"} - shouldUploadVideo: true - }) - .then -> - expect(upload.send.callCount).to.be.eq(3) - expect(upload.send.firstCall).to.be.calledWith("path/to/video", "https://s3.upload.video") - expect(upload.send.secondCall).to.be.calledWith("path/to/screenshot1", "https://s3.upload.screenshot/1") - expect(upload.send.thirdCall).to.be.calledWith("path/to/screenshot2", "https://s3.upload.screenshot/2") - - ## reset the stub - upload.send.reset() - - ## does not upload the video - record.uploadAssets("id-123", { - tests: 1 - passes: 2 - failures: 3 - pending: 4 - duration: 5 - video: "path/to/video" - screenshots: [{ - clientId: 1 - name: "foo" - path: "path/to/screenshot1" - }, { - clientId: 2 - name: "bar" - path: "path/to/screenshot2" - }] - failingTests: ["foo"] - config: {foo: "bar"} - shouldUploadVideo: false - }) - .then -> - expect(upload.send.callCount).to.be.eq(2) - expect(upload.send.firstCall).to.be.calledWith("path/to/screenshot1", "https://s3.upload.screenshot/1") - expect(upload.send.secondCall).to.be.calledWith("path/to/screenshot2", "https://s3.upload.screenshot/2") - - it "logs warning on error", -> - err = new Error("foo") - - @sandbox.spy(errors, "warning") - @sandbox.spy(logger, "createException") - @sandbox.spy(console, "log") - - api.updateInstance.rejects(err) - - record.uploadAssets("id-123", {}) - .then -> - expect(errors.warning).to.be.calledWith("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - expect(console.log).to.be.calledWithMatch("This run will not be recorded.") - expect(console.log).to.be.calledWithMatch("Error: foo") - expect(logger.createException).to.be.calledWith(err) - - it "does not createException when statusCode is 503", -> - err = new Error("foo") - err.statusCode = 503 - - @sandbox.spy(logger, "createException") - - api.updateInstance.rejects(err) - - record.uploadAssets("id-123", {}) + recordMode.updateInstanceStdout(options) .then -> expect(logger.createException).not.to.be.called context ".createInstance", -> beforeEach -> - @sandbox.stub(api, "createInstance") + sinon.stub(api, "createInstance") it "calls api.createInstance", -> api.createInstance.resolves() - record.createInstance("id-123", "cypress/integration/app_spec.coffee", "FooBrowser") - - expect(api.createInstance).to.be.calledWith({ - buildId: "id-123" - browser: "FooBrowser" - spec: "cypress/integration/app_spec.coffee" + recordMode.createInstance({ + runId: "run-123", + planId: "plan-123" + machineId: "machine-123" + platform: {} + spec: { path: "cypress/integration/app_spec.coffee" } }) - - it "logs warning on error", -> - err = new Error("foo") - - @sandbox.spy(errors, "warning") - @sandbox.spy(logger, "createException") - @sandbox.spy(console, "log") - - api.createInstance.rejects(err) - - record.createInstance("id-123", null) - .then (ret) -> - expect(ret).to.be.null - expect(errors.warning).to.be.calledWith("DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE", err) - expect(console.log).to.be.calledWithMatch("This run will not be recorded.") - expect(console.log).to.be.calledWithMatch("Error: foo") - expect(logger.createException).to.be.calledWith(err) + .then -> + expect(api.createInstance).to.be.calledWith({ + runId: "run-123", + planId: "plan-123" + machineId: "machine-123" + platform: {} + spec: "cypress/integration/app_spec.coffee" + }) it "does not createException when statusCode is 503", -> err = new Error("foo") err.statusCode = 503 - @sandbox.spy(logger, "createException") + sinon.spy(logger, "createException") api.createInstance.rejects(err) - record.createInstance("id-123", null) + recordMode.createInstance({ + runId: "run-123", + planId: "plan-123" + machineId: "machine-123" + platform: {} + spec: { path: "cypress/integration/app_spec.coffee" } + }) .then (ret) -> expect(ret).to.be.null expect(logger.createException).not.to.be.called - - context ".run", -> - beforeEach -> - @sandbox.stub(record, "generateProjectBuildId").resolves("build-id-123") - @sandbox.stub(record, "createInstance").resolves("instance-id-123") - @sandbox.stub(record, "uploadAssets").resolves() - @sandbox.stub(record, "uploadStdout").resolves() - @sandbox.stub(Project, "id").resolves("id-123") - @sandbox.stub(Project, "config").resolves({projectName: "projectName"}) - @sandbox.stub(headless, "run").resolves({tests: 2, passes: 1}) - @sandbox.spy(Project, "add") - - it "ensures id", -> - record.run({projectPath: "/_test-output/path/to/project"}) - .then -> - expect(Project.id).to.be.calledWith("/_test-output/path/to/project") - - it "adds project with projectPath", -> - record.run({projectPath: "/_test-output/path/to/project"}) - .then -> - expect(Project.add).to.be.calledWith("/_test-output/path/to/project") - - it "passes id + projectPath + options.key to generateProjectBuildId", -> - record.run({projectPath: "/_test-output/path/to/project", key: "key-foo"}) - .then -> - expect(record.generateProjectBuildId).to.be.calledWith("id-123", "/_test-output/path/to/project", "projectName", "key-foo") - - it "passes buildId + options.spec to createInstance", -> - record.run({spec: "foo/bar/spec"}) - .then -> - expect(record.createInstance).to.be.calledWith("build-id-123", "foo/bar/spec") - - it "does not call record.createInstance or record.uploadAssets when no buildId", -> - record.generateProjectBuildId.resolves(null) - - record.run({}) - .then (stats) -> - expect(record.createInstance).not.to.be.called - expect(record.uploadAssets).not.to.be.called - - expect(stats).to.deep.eq({ - tests: 2 - passes: 1 - }) - - it "calls headless.run + ensureAuthToken + allDone into options", -> - opts = {foo: "bar"} - - record.run(opts) - .then -> - expect(headless.run).to.be.calledWith({projectId: "id-123", foo: "bar", ensureAuthToken: false, allDone: false}) - - it "calls uploadAssets with instanceId, stats, and stdout", -> - @sandbox.stub(stdout, "capture").returns({ - toString: -> "foobarbaz" - }) - - record.run({}) - .then -> - expect(record.uploadAssets).to.be.calledWith("instance-id-123", {tests: 2, passes: 1}, "foobarbaz") - - it "does not call uploadAssets with no instanceId", -> - record.createInstance.resolves(null) - - record.run({}) - .then (stats) -> - expect(record.uploadAssets).not.to.be.called - - expect(stats).to.deep.eq({ - tests: 2 - passes: 1 - }) - - it "does not call uploadStdout with no instanceId", -> - record.createInstance.resolves(null) - - record.run({}) - .then (stats) -> - expect(record.uploadStdout).not.to.be.called - - it "does not call uploadStdout on uploadAssets failure", -> - record.uploadAssets.restore() - @sandbox.stub(api, "updateInstance").rejects(new Error) - - record.run({}) - .then (stats) -> - expect(record.uploadStdout).not.to.be.called - - it "calls record.uploadStdout on uploadAssets success", -> - @sandbox.stub(stdout, "capture").returns({ - toString: -> "foobarbaz" - }) - - record.run({}) - .then (stats) -> - expect(record.uploadStdout).to.be.calledWith("instance-id-123", "foobarbaz") - - it "captures stdout from headless.run and headless.allDone", -> - fn = -> - console.log("foo") - console.log("bar") - process.stdout.write("baz") - - Promise.resolve({failures: 0}) - - headless.run.restore() - @sandbox.stub(headless, "run", fn) - - record.run({}) - .then (stats) -> - str = record.uploadStdout.getCall(0).args[1] - - expect(str).to.include("foo\nbar\nbaz") - expect(str).to.include("All Done") - - it "calls headless.allDone on uploadAssets success", -> - @sandbox.spy(terminal, "header") - - record.run({}) - .then (stats) -> - expect(terminal.header).to.be.calledWith("All Done") - - expect(stats).to.deep.eq({ - tests: 2 - passes: 1 - }) - - it "calls headless.allDone on uploadAssets failure", -> - @sandbox.spy(terminal, "header") - @sandbox.stub(api, "updateInstance").rejects(new Error) - record.uploadAssets.restore() - - record.run({}) - .then (stats) -> - expect(terminal.header).to.be.calledWith("All Done") - - expect(stats).to.deep.eq({ - tests: 2 - passes: 1 - }) - - it "calls headless.allDone on createInstance failure", -> - @sandbox.spy(terminal, "header") - record.createInstance.resolves(null) - - record.run({}) - .then (stats) -> - expect(terminal.header).to.be.calledWith("All Done") - - expect(stats).to.deep.eq({ - tests: 2 - passes: 1 - }) diff --git a/packages/server/test/unit/modes/run_spec.coffee b/packages/server/test/unit/modes/run_spec.coffee new file mode 100644 index 000000000000..12796c821111 --- /dev/null +++ b/packages/server/test/unit/modes/run_spec.coffee @@ -0,0 +1,614 @@ +require("../../spec_helper") + +Promise = require("bluebird") +electron = require("electron") +user = require("#{root}../lib/user") +video = require("#{root}../lib/video") +errors = require("#{root}../lib/errors") +config = require("#{root}../lib/config") +Project = require("#{root}../lib/project") +browsers = require("#{root}../lib/browsers") +Reporter = require("#{root}../lib/reporter") +runMode = require("#{root}../lib/modes/run") +openProject = require("#{root}../lib/open_project") +env = require("#{root}../lib/util/env") +random = require("#{root}../lib/util/random") +system = require("#{root}../lib/util/system") +specsUtil = require("#{root}../lib/util/specs") + +describe "lib/modes/run", -> + beforeEach -> + @projectInstance = Project("/_test-output/path/to/project") + + context ".getProjectId", -> + it "resolves if id", -> + runMode.getProjectId("project", "id123") + .then (ret) -> + expect(ret).to.eq("id123") + + it "resolves if CYPRESS_PROJECT_ID set", -> + sinon.stub(env, "get").withArgs("CYPRESS_PROJECT_ID").returns("envId123") + + runMode.getProjectId("project") + .then (ret) -> + expect(ret).to.eq("envId123") + + it "is null when no projectId", -> + project = { + getProjectId: sinon.stub().rejects(new Error) + } + + runMode.getProjectId(project) + .then (ret) -> + expect(ret).to.be.null + + context ".openProjectCreate", -> + beforeEach -> + sinon.stub(openProject, "create").resolves() + + options = { + port: 8080 + env: {foo: "bar"} + projectRoot: "/_test-output/path/to/project/foo" + } + + runMode.openProjectCreate(options.projectRoot, 1234, options) + + it "calls openProject.create with projectRoot + options", -> + expect(openProject.create).to.be.calledWithMatch("/_test-output/path/to/project/foo", { + port: 8080 + projectRoot: "/_test-output/path/to/project/foo" + env: {foo: "bar"} + }, { + morgan: false + socketId: 1234 + report: true + isTextTerminal: true + }) + + it "emits 'exitEarlyWithErr' with error message onError", -> + sinon.stub(openProject, "emit") + expect(openProject.create.lastCall.args[2].onError).to.be.a("function") + openProject.create.lastCall.args[2].onError({ message: "the message" }) + expect(openProject.emit).to.be.calledWith("exitEarlyWithErr", "the message") + + context ".getElectronProps", -> + it "sets width and height", -> + props = runMode.getElectronProps() + + expect(props.width).to.eq(1280) + expect(props.height).to.eq(720) + + it "sets show to boolean", -> + props = runMode.getElectronProps(false) + expect(props.show).to.be.false + + props = runMode.getElectronProps(true) + expect(props.show).to.be.true + + it "sets recordFrameRate and onPaint when write is true", -> + write = sinon.stub() + + image = { + toJPEG: sinon.stub().returns("imgdata") + } + + props = runMode.getElectronProps(true, {}, write) + + expect(props.recordFrameRate).to.eq(20) + + props.onPaint({}, false, image) + + expect(write).to.be.calledWith("imgdata") + + it "does not set recordFrameRate or onPaint when write is falsy", -> + props = runMode.getElectronProps(true, {}, false) + + expect(props).not.to.have.property("recordFrameRate") + expect(props).not.to.have.property("onPaint") + + it "sets options.show = false onNewWindow callback", -> + options = {show: true} + + props = runMode.getElectronProps() + props.onNewWindow(null, null, null, null, options) + + expect(options.show).to.eq(false) + + it "emits exitEarlyWithErr when webContents crashed", -> + sinon.spy(errors, "get") + sinon.spy(errors, "log") + + emit = sinon.stub(@projectInstance, "emit") + + props = runMode.getElectronProps(true, @projectInstance) + + props.onCrashed() + + expect(errors.get).to.be.calledWith("RENDERER_CRASHED") + expect(errors.log).to.be.calledOnce + expect(emit).to.be.calledWithMatch("exitEarlyWithErr", "We detected that the Chromium Renderer process just crashed.") + + context ".launchBrowser", -> + beforeEach -> + @launch = sinon.stub(openProject, "launch") + sinon.stub(runMode, "getElectronProps").returns({foo: "bar"}) + sinon.stub(runMode, "screenshotMetadata").returns({a: "a"}) + + it "can launch electron", -> + screenshots = [] + + runMode.launchBrowser({ + browserName: "electron" + project: @projectInstance + write: "write" + gui: null + screenshots: screenshots + spec: { + absolute: "/path/to/spec" + } + }) + + expect(runMode.getElectronProps).to.be.calledWith(false, @projectInstance, "write") + + expect(@launch).to.be.calledWithMatch("electron", "/path/to/spec", {foo: "bar"}) + + browserOpts = @launch.firstCall.args[2] + + onAfterResponse = browserOpts.automationMiddleware.onAfterResponse + + expect(onAfterResponse).to.be.a("function") + + onAfterResponse("take:screenshot", {}, {}) + onAfterResponse("get:cookies") + + expect(screenshots).to.deep.eq([{a: "a"}]) + + it "can launch chrome", -> + runMode.launchBrowser({ + browserName: "chrome" + spec: { + absolute: "/path/to/spec" + } + }) + + expect(runMode.getElectronProps).not.to.be.called + + expect(@launch).to.be.calledWithMatch("chrome", "/path/to/spec", {}) + + context ".postProcessRecording", -> + beforeEach -> + sinon.stub(video, "process").resolves() + + it "calls video process with name, cname and videoCompression", -> + end = -> Promise.resolve() + + runMode.postProcessRecording(end, "foo", "foo-compress", 32, true) + .then -> + expect(video.process).to.be.calledWith("foo", "foo-compress", 32) + + it "does not call video process when videoCompression is false", -> + end = -> Promise.resolve() + + runMode.postProcessRecording(end, "foo", "foo-compress", false, true) + .then -> + expect(video.process).not.to.be.called + + it "calls video process if we have been told to upload videos", -> + end = -> Promise.resolve() + + runMode.postProcessRecording(end, "foo", "foo-compress", 32, true) + .then -> + expect(video.process).to.be.calledWith("foo", "foo-compress", 32) + + it "does not call video process if there are no failing tests and we have set not to upload video on passing", -> + end = -> Promise.resolve() + + runMode.postProcessRecording(end, "foo", "foo-compress", 32, false) + .then -> + expect(video.process).not.to.be.called + + context ".waitForBrowserToConnect", -> + it "throws TESTS_DID_NOT_START_FAILED after 3 connection attempts", -> + sinon.spy(errors, "warning") + sinon.spy(errors, "get") + sinon.spy(openProject, "closeBrowser") + sinon.stub(runMode, "launchBrowser").resolves() + sinon.stub(runMode, "waitForSocketConnection").callsFake -> + Promise.delay(1000) + + emit = sinon.stub(@projectInstance, "emit") + + runMode.waitForBrowserToConnect({project: @projectInstance, timeout: 10}) + .then -> + expect(openProject.closeBrowser).to.be.calledThrice + expect(runMode.launchBrowser).to.be.calledThrice + expect(errors.warning).to.be.calledWith("TESTS_DID_NOT_START_RETRYING", "Retrying...") + expect(errors.warning).to.be.calledWith("TESTS_DID_NOT_START_RETRYING", "Retrying again...") + expect(errors.get).to.be.calledWith("TESTS_DID_NOT_START_FAILED") + expect(emit).to.be.calledWith("exitEarlyWithErr", "The browser never connected. Something is wrong. The tests cannot run. Aborting...") + + context ".waitForSocketConnection", -> + beforeEach -> + @projectStub = sinon.stub({ + on: -> + removeListener: -> + }) + + it "attaches fn to 'socket:connected' event", -> + runMode.waitForSocketConnection(@projectStub, 1234) + expect(@projectStub.on).to.be.calledWith("socket:connected") + + it "calls removeListener if socketId matches id", -> + @projectStub.on.yields(1234) + + runMode.waitForSocketConnection(@projectStub, 1234) + .then => + expect(@projectStub.removeListener).to.be.calledWith("socket:connected") + + describe "integration", -> + it "resolves undefined when socket:connected fires", -> + process.nextTick => + @projectInstance.emit("socket:connected", 1234) + + runMode.waitForSocketConnection(@projectInstance, 1234) + .then (ret) -> + expect(ret).to.be.undefined + + it "does not resolve if socketId does not match id", -> + process.nextTick => + @projectInstance.emit("socket:connected", 12345) + + runMode + .waitForSocketConnection(@projectInstance, 1234) + .timeout(50) + .then -> + throw new Error("should time out and not resolve") + .catch Promise.TimeoutError, (err) -> + + it "actually removes the listener", -> + process.nextTick => + @projectInstance.emit("socket:connected", 12345) + expect(@projectInstance.listeners("socket:connected")).to.have.length(1) + @projectInstance.emit("socket:connected", "1234") + expect(@projectInstance.listeners("socket:connected")).to.have.length(1) + @projectInstance.emit("socket:connected", 1234) + expect(@projectInstance.listeners("socket:connected")).to.have.length(0) + + runMode.waitForSocketConnection(@projectInstance, 1234) + + context ".waitForTestsToFinishRunning", -> + beforeEach -> + sinon.stub(@projectInstance, "getConfig").resolves({}) + + it "end event resolves with obj, displays stats, displays screenshots, sets video timestamps", -> + started = new Date + screenshots = [{}, {}, {}] + cfg = {} + end = -> + results = { + tests: [4,5,6] + stats: { + tests: 1 + passes: 2 + failures: 3 + pending: 4 + duration: 5 + } + } + + sinon.stub(Reporter, "setVideoTimestamp") + .withArgs(started, results.tests) + .returns([1,2,3]) + + sinon.stub(runMode, "postProcessRecording").resolves() + sinon.spy(runMode, "displayResults") + sinon.spy(runMode, "displayScreenshots") + + process.nextTick => + @projectInstance.emit("end", results) + + runMode.waitForTestsToFinishRunning({ + project: @projectInstance, + name: "foo.mp4" + cname: "foo-compressed.mp4" + videoCompression: 32 + videoUploadOnPasses: true + gui: false + screenshots + started + end + spec: { + path: "cypress/integration/spec.js" + } + }) + .then (obj) -> + expect(runMode.postProcessRecording).to.be.calledWith(end, "foo.mp4", "foo-compressed.mp4", 32, true) + + expect(runMode.displayResults).to.be.calledWith(results) + expect(runMode.displayScreenshots).to.be.calledWith(screenshots) + + expect(obj).to.deep.eq({ + screenshots + video: "foo.mp4" + error: null + hooks: null + reporterStats: null + shouldUploadVideo: true + tests: [1,2,3] + spec: { + path: "cypress/integration/spec.js" + } + stats: { + tests: 1 + passes: 2 + failures: 3 + pending: 4 + duration: 5 + } + }) + + it "exitEarlyWithErr event resolves with no tests, and error", -> + clock = sinon.useFakeTimers() + + err = new Error("foo") + started = new Date + wallClock = new Date() + screenshots = [{}, {}, {}] + end = -> + + sinon.stub(runMode, "postProcessRecording").resolves() + sinon.spy(runMode, "displayResults") + sinon.spy(runMode, "displayScreenshots") + + process.nextTick => + expect(@projectInstance.listeners("exitEarlyWithErr")).to.have.length(1) + @projectInstance.emit("exitEarlyWithErr", err.message) + expect(@projectInstance.listeners("exitEarlyWithErr")).to.have.length(0) + + runMode.waitForTestsToFinishRunning({ + project: @projectInstance, + name: "foo.mp4" + cname: "foo-compressed.mp4" + videoCompression: 32 + videoUploadOnPasses: true + gui: false + screenshots + started + end + spec: { + path: "cypress/integration/spec.js" + } + }) + .then (obj) -> + expect(runMode.postProcessRecording).to.be.calledWith(end, "foo.mp4", "foo-compressed.mp4", 32, true) + + expect(runMode.displayResults).to.be.calledWith(obj) + expect(runMode.displayScreenshots).to.be.calledWith(screenshots) + + expect(obj).to.deep.eq({ + screenshots + error: err.message + video: "foo.mp4" + hooks: null + tests: null + reporterStats: null + shouldUploadVideo: true + spec: { + path: "cypress/integration/spec.js" + } + stats: { + failures: 1 + tests: 0 + passes: 0 + pending: 0 + suites: 0 + skipped: 0 + wallClockDuration: 0 + wallClockStartedAt: wallClock.toJSON() + wallClockEndedAt: wallClock.toJSON() + } + }) + + it "should not upload video when videoUploadOnPasses is false and no failures", -> + process.nextTick => + @projectInstance.emit("end", { + stats: { + failures: 0 + } + }) + + sinon.spy(runMode, "postProcessRecording") + sinon.spy(video, "process") + end = sinon.stub().resolves() + + runMode.waitForTestsToFinishRunning({ + project: @projectInstance, + name: "foo.mp4" + cname: "foo-compressed.mp4" + videoCompression: 32 + videoUploadOnPasses: false + gui: false + end + }) + .then (obj) -> + expect(runMode.postProcessRecording).to.be.calledWith(end, "foo.mp4", "foo-compressed.mp4", 32, false) + + expect(video.process).not.to.be.called + + context ".listenForProjectEnd", -> + it "resolves with end event + argument", -> + process.nextTick => + @projectInstance.emit("end", {foo: "bar"}) + + runMode.listenForProjectEnd(@projectInstance) + .then (obj) -> + expect(obj).to.deep.eq({ + foo: "bar" + }) + + it "stops listening to end event", -> + process.nextTick => + expect(@projectInstance.listeners("end")).to.have.length(1) + @projectInstance.emit("end", {foo: "bar"}) + expect(@projectInstance.listeners("end")).to.have.length(0) + + runMode.listenForProjectEnd(@projectInstance) + + context ".run browser vs video recording", -> + beforeEach -> + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(user, "ensureAuthToken") + sinon.stub(Project, "ensureExists").resolves() + sinon.stub(random, "id").returns(1234) + sinon.stub(openProject, "create").resolves(openProject) + sinon.stub(runMode, "waitForSocketConnection").resolves() + sinon.stub(runMode, "waitForTestsToFinishRunning").resolves({ + stats: { failures: 10 } + spec: {} + }) + sinon.spy(runMode, "waitForBrowserToConnect") + sinon.stub(video, "start").resolves() + sinon.stub(openProject, "launch").resolves() + sinon.stub(openProject, "getProject").resolves(@projectInstance) + sinon.spy(errors, "warning") + sinon.stub(config, "get").resolves({ + proxyUrl: "http://localhost:12345", + videoRecording: true, + videosFolder: "videos", + integrationFolder: "/path/to/integrationFolder" + }) + sinon.stub(specsUtil, "find").resolves([ + { + name: "foo_spec.js" + path: "cypress/integration/foo_spec.js" + absolute: "/path/to/spec.js" + } + ]) + + it "shows no warnings for default browser", -> + runMode.run() + .then -> + expect(errors.warning).to.not.be.called + + it "shows no warnings for electron browser", -> + runMode.run({browser: "electron"}) + .then -> + expect(errors.warning).to.not.be.calledWith("CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER") + + it "disables video recording on interactive mode runs", -> + runMode.run({headed: true}) + .then -> + expect(errors.warning).to.be.calledWith("CANNOT_RECORD_VIDEO_HEADED") + + it "disables video recording for non-electron browser", -> + runMode.run({browser: "chrome"}) + .then -> + expect(errors.warning).to.be.calledWith("CANNOT_RECORD_VIDEO_FOR_THIS_BROWSER") + + it "names video file with spec name", -> + runMode.run() + .then => + expect(video.start).to.be.calledWith("videos/foo_spec.js.mp4") + expect(runMode.waitForTestsToFinishRunning).to.be.calledWithMatch({ + cname: "videos/foo_spec.js-compressed.mp4" + }) + + context ".run", -> + beforeEach -> + sinon.stub(@projectInstance, "getConfig").resolves({ + proxyUrl: "http://localhost:12345" + }) + sinon.stub(electron.app, "on").withArgs("ready").yieldsAsync() + sinon.stub(user, "ensureAuthToken") + sinon.stub(Project, "ensureExists").resolves() + sinon.stub(random, "id").returns(1234) + sinon.stub(openProject, "create").resolves(openProject) + sinon.stub(system, "info").resolves({ osName: "osFoo", osVersion: "fooVersion" }) + sinon.stub(browsers, "ensureAndGetByName").resolves({ + name: "fooBrowser", + path: "path/to/browser" + version: "777" + }) + sinon.stub(runMode, "waitForSocketConnection").resolves() + sinon.stub(runMode, "waitForTestsToFinishRunning").resolves({ + stats: { failures: 10 } + spec: {} + }) + sinon.spy(runMode, "waitForBrowserToConnect") + sinon.spy(runMode, "runSpecs") + sinon.stub(openProject, "launch").resolves() + sinon.stub(openProject, "getProject").resolves(@projectInstance) + sinon.stub(specsUtil, "find").resolves([ + { + name: "foo_spec.js" + path: "cypress/integration/foo_spec.js" + absolute: "/path/to/spec.js" + } + ]) + + it "no longer ensures user session", -> + runMode.run() + .then -> + expect(user.ensureAuthToken).not.to.be.called + + it "resolves with object and totalFailed", -> + runMode.run() + .then (results) -> + expect(results).to.have.property("totalFailed", 10) + + it "passes projectRoot + options to openProject", -> + opts = { projectRoot: "/path/to/project", foo: "bar" } + + runMode.run(opts) + .then -> + expect(openProject.create).to.be.calledWithMatch(opts.projectRoot, opts) + + it "passes project + id to waitForBrowserToConnect", -> + runMode.run() + .then => + expect(runMode.waitForBrowserToConnect).to.be.calledWithMatch({ + project: @projectInstance + socketId: 1234 + }) + + it "passes project to waitForTestsToFinishRunning", -> + runMode.run() + .then => + expect(runMode.waitForTestsToFinishRunning).to.be.calledWithMatch({ + project: @projectInstance + }) + + it "passes headed to openProject.launch", -> + browsers.ensureAndGetByName.resolves({ name: "electron" }) + + runMode.run({headed: true}) + .then -> + expect(openProject.launch).to.be.calledWithMatch( + "electron", + "path/to/spec.js", + { + show: true + } + ) + + it "passes sys to runSpecs", -> + runMode.run() + .then -> + expect(runMode.runSpecs).to.be.calledWithMatch({ + sys: { + osName: "osFoo" + osVersion: "fooVersion" + } + }) + + it "passes browser to runSpecs", -> + runMode.run() + .then -> + expect(runMode.runSpecs).to.be.calledWithMatch({ + browser: { + name: "fooBrowser", + path: "path/to/browser" + version: "777" + } + }) diff --git a/packages/server/test/unit/open_project_spec.coffee b/packages/server/test/unit/open_project_spec.coffee new file mode 100644 index 000000000000..bf6a274395ab --- /dev/null +++ b/packages/server/test/unit/open_project_spec.coffee @@ -0,0 +1,39 @@ +require("../spec_helper") + +browsers = require("#{root}lib/browsers") +Project = require("#{root}lib/project") +openProject = require("#{root}lib/open_project") +preprocessor = require("#{root}lib/plugins/preprocessor") + +describe "lib/open_project", -> + beforeEach -> + sinon.stub(browsers, "get").resolves() + sinon.stub(browsers, "open") + sinon.stub(Project.prototype, "open") + sinon.stub(Project.prototype, "reset").resolves() + sinon.stub(Project.prototype, "getSpecUrl").resolves() + sinon.stub(Project.prototype, "getConfig").resolves({}) + sinon.stub(Project.prototype, "getAutomation") + sinon.stub(preprocessor, "removeFile") + + openProject.create("/project/root") + + context "#launch", -> + + it "tells preprocessor to remove file on browser close", -> + openProject.launch("chrome", "path/to/spec").then -> + browsers.open.lastCall.args[1].onBrowserClose() + expect(preprocessor.removeFile).to.be.calledWith("path/to/spec") + + + it "does not tell preprocessor to remove file if no spec", -> + openProject.launch("chrome").then -> + browsers.open.lastCall.args[1].onBrowserClose() + expect(preprocessor.removeFile).not.to.be.called + + it "runs original onBrowserClose callback on browser close", -> + onBrowserClose = sinon.stub() + options = { onBrowserClose } + openProject.launch("chrome", "path/to/spec", options).then -> + browsers.open.lastCall.args[1].onBrowserClose() + expect(onBrowserClose).to.be.called diff --git a/packages/server/test/unit/open_spec.coffee b/packages/server/test/unit/open_spec.coffee index bb985028c8d9..f36ff39ed8a5 100644 --- a/packages/server/test/unit/open_spec.coffee +++ b/packages/server/test/unit/open_spec.coffee @@ -12,14 +12,14 @@ describe "lib/util/open", -> beforeEach -> @platform = process.platform - cpStub = @sandbox.stub({ + cpStub = sinon.stub({ once: -> unref: -> }) cpStub.once.withArgs("close").yieldsAsync(0) - @sandbox.stub(cp, "spawn").returns(cpStub) + sinon.stub(cp, "spawn").returns(cpStub) afterEach -> ## reset the platform diff --git a/packages/server/test/unit/plugins/child/preprocessor_spec.coffee b/packages/server/test/unit/plugins/child/preprocessor_spec.coffee index fa6e1f1d32f3..ea3a64145d0d 100644 --- a/packages/server/test/unit/plugins/child/preprocessor_spec.coffee +++ b/packages/server/test/unit/plugins/child/preprocessor_spec.coffee @@ -8,59 +8,77 @@ preprocessor = require("#{root}../../lib/plugins/child/preprocessor") describe "lib/plugins/child/preprocessor", -> beforeEach -> @ipc = { - send: @sandbox.spy() - on: @sandbox.stub() - removeListener: @sandbox.spy() + send: sinon.spy() + on: sinon.stub() + removeListener: sinon.spy() } - @invoke = @sandbox.spy() + @invoke = sinon.spy() @ids = {} - @config = { + @file = { filePath: 'file/path' outputPath: 'output/path' shouldWatch: true } + @file2 = { + filePath: 'file2/path' + outputPath: 'output/path2' + shouldWatch: true + } - @sandbox.stub(util, "wrapChildPromise") + sinon.stub(util, "wrapChildPromise") - preprocessor.wrap(@ipc, @invoke, @ids, [@config]) + preprocessor.wrap(@ipc, @invoke, @ids, [@file]) afterEach -> - ## clear out configs state - @ipc.on.withArgs("preprocessor:close").yield(@config.filePath) + preprocessor._clearFiles() it "passes through ipc, invoke function, and ids", -> expect(util.wrapChildPromise).to.be.calledWith(@ipc, @invoke, @ids) - it "passes through simple config values", -> - config = util.wrapChildPromise.lastCall.args[3][0] - expect(config.filePath).to.equal(@config.filePath) - expect(config.outputPath).to.equal(@config.outputPath) - expect(config.shouldWatch).to.equal(@config.shouldWatch) + it "passes through simple file values", -> + file = util.wrapChildPromise.lastCall.args[3][0] + expect(file.filePath).to.equal(@file.filePath) + expect(file.outputPath).to.equal(@file.outputPath) + expect(file.shouldWatch).to.equal(@file.shouldWatch) - it "enhances config with event emitter", -> + it "re-applies event emitter methods to file", -> expect(util.wrapChildPromise.lastCall.args[3][0]).to.be.an.instanceOf(EE) it "sends 'preprocessor:rerun' through ipc on 'rerun' event", -> - config = util.wrapChildPromise.lastCall.args[3][0] - config.emit("rerun") - expect(@ipc.send).to.be.calledWith("preprocessor:rerun", @config.filePath) + file = util.wrapChildPromise.lastCall.args[3][0] + file.emit("rerun") + expect(@ipc.send).to.be.calledWith("preprocessor:rerun", @file.filePath) - it "emits 'close' on config when ipc emits 'preprocessor:close' with same file path", -> - config = util.wrapChildPromise.lastCall.args[3][0] - handler = @sandbox.spy() - config.on("close", handler) - @ipc.on.withArgs("preprocessor:close").yield(@config.filePath) + it "emits 'close' when ipc emits 'preprocessor:close' with same file path", -> + file = util.wrapChildPromise.lastCall.args[3][0] + handler = sinon.spy() + file.on("close", handler) + @ipc.on.withArgs("preprocessor:close").yield(@file.filePath) expect(handler).to.be.called - it "does not 'close' on config when ipc emits 'preprocessor:close' with different file path", -> - config = util.wrapChildPromise.lastCall.args[3][0] - handler = @sandbox.spy() - config.on("close", handler) + it "does not close file when ipc emits 'preprocessor:close' with different file path", -> + file = util.wrapChildPromise.lastCall.args[3][0] + handler = sinon.spy() + file.on("close", handler) @ipc.on.withArgs("preprocessor:close").yield("different/path") expect(handler).not.to.be.called - it "passes existing config if called again with same file path", -> - preprocessor.wrap(@ipc, @invoke, @ids, [@config]) - config1 = util.wrapChildPromise.firstCall.args[3][0] - config2 = util.wrapChildPromise.lastCall.args[3][0] - expect(config1).to.equal(config2) + it "passes existing file if called again with same file path", -> + preprocessor.wrap(@ipc, @invoke, @ids, [@file]) + file1 = util.wrapChildPromise.firstCall.args[3][0] + file2 = util.wrapChildPromise.lastCall.args[3][0] + expect(file1).to.equal(file2) + + it "deletes stored file objects on close(filePath)", -> + preprocessor.wrap(@ipc, @invoke, @ids, [@file2]) + @ipc.on.withArgs("preprocessor:close").yield(@file.filePath) + files = preprocessor._getFiles() + expect(Object.keys(files).length).to.equal(1) + expect(files[@file2.path]).to.be.defined + expect(files[@file.path]).to.be.undefined + + it "deletes all stored file objects on close()", -> + preprocessor.wrap(@ipc, @invoke, @ids, [@file2]) + @ipc.on.withArgs("preprocessor:close").yield() + files = preprocessor._getFiles() + expect(Object.keys(files).length).to.equal(0) diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee index 9fcc388037f0..37fbb13de3ec 100644 --- a/packages/server/test/unit/plugins/child/run_plugins_spec.coffee +++ b/packages/server/test/unit/plugins/child/run_plugins_spec.coffee @@ -5,7 +5,9 @@ cp = require("child_process") snapshot = require("snap-shot-it") preprocessor = require("#{root}../../lib/plugins/child/preprocessor") +task = require("#{root}../../lib/plugins/child/task") runPlugins = require("#{root}../../lib/plugins/child/run_plugins") +util = require("#{root}../../lib/plugins/util") colorCodeRe = /\[[0-9;]+m/gm pathRe = /\/?([a-z0-9_-]+\/)*[a-z0-9_-]+\/([a-z_]+\.\w+)[:0-9]+/gmi @@ -19,9 +21,9 @@ withoutStackPaths = (stack) -> stack.replace(stackPathRe, '$2') describe "lib/plugins/child/run_plugins", -> beforeEach -> @ipc = { - send: @sandbox.spy() - on: @sandbox.stub() - removeListener: @sandbox.spy() + send: sinon.spy() + on: sinon.stub() + removeListener: sinon.spy() } afterEach -> @@ -57,7 +59,7 @@ describe "lib/plugins/child/run_plugins", -> describe "on 'load' message", -> it "sends error if pluginsFile function rejects the promise", (done) -> err = new Error('foo') - pluginsFn = @sandbox.stub().rejects(err) + pluginsFn = sinon.stub().rejects(err) mockery.registerMock("plugins-file", pluginsFn) @ipc.on.withArgs("load").yields({}) @@ -71,7 +73,7 @@ describe "lib/plugins/child/run_plugins", -> done() it "calls function exported by pluginsFile with register function and config", -> - pluginsFn = @sandbox.spy() + pluginsFn = sinon.spy() mockery.registerMock("plugins-file", pluginsFn) runPlugins(@ipc, "plugins-file") config = {} @@ -96,17 +98,21 @@ describe "lib/plugins/child/run_plugins", -> describe "on 'execute' message", -> beforeEach -> - @sandbox.stub(preprocessor, "wrap") - @onFilePreprocessor = @sandbox.stub().resolves() + sinon.stub(preprocessor, "wrap") + @onFilePreprocessor = sinon.stub().resolves() + @beforeBrowserLaunch = sinon.stub().resolves() + @taskRequested = sinon.stub().resolves("foo") pluginsFn = (register) => register("file:preprocessor", @onFilePreprocessor) + register("before:browser:launch", @beforeBrowserLaunch) + register("task", @taskRequested) mockery.registerMock("plugins-file", pluginsFn) runPlugins(@ipc, "plugins-file") @ipc.on.withArgs("load").yield() context "file:preprocessor", -> beforeEach -> - @ids = { callbackId: 0, invocationId: "00" } + @ids = { eventId: 0, invocationId: "00" } it "calls preprocessor handler", -> args = ["arg1", "arg2"] @@ -119,14 +125,47 @@ describe "lib/plugins/child/run_plugins", -> it "invokes registered function when invoked by preprocessor handler", -> @ipc.on.withArgs("execute").yield("file:preprocessor", @ids, []) - args = ["one", "two"] - preprocessor.wrap.lastCall.args[1](0, args) + preprocessor.wrap.lastCall.args[1](2, ["one", "two"]) expect(@onFilePreprocessor).to.be.calledWith("one", "two") + context "before:browser:launch", -> + beforeEach -> + sinon.stub(util, "wrapChildPromise") + @ids = { eventId: 1, invocationId: "00" } + + it "wraps child promise", -> + args = ["arg1", "arg2"] + @ipc.on.withArgs("execute").yield("before:browser:launch", @ids, args) + expect(util.wrapChildPromise).to.be.called + expect(util.wrapChildPromise.lastCall.args[0]).to.equal(@ipc) + expect(util.wrapChildPromise.lastCall.args[1]).to.be.a("function") + expect(util.wrapChildPromise.lastCall.args[2]).to.equal(@ids) + expect(util.wrapChildPromise.lastCall.args[3]).to.equal(args) + + it "invokes registered function when invoked by preprocessor handler", -> + @ipc.on.withArgs("execute").yield("before:browser:launch", @ids, []) + args = ["one", "two"] + util.wrapChildPromise.lastCall.args[1](3, args) + expect(@beforeBrowserLaunch).to.be.calledWith("one", "two") + + context "task", -> + beforeEach -> + sinon.stub(task, "wrap") + @ids = { eventId: 5, invocationId: "00" } + + it "calls task handler", -> + args = ["arg1"] + @ipc.on.withArgs("execute").yield("task", @ids, args) + expect(task.wrap).to.be.called + expect(task.wrap.lastCall.args[0]).to.equal(@ipc) + expect(task.wrap.lastCall.args[1]).to.be.an("object") + expect(task.wrap.lastCall.args[2]).to.equal(@ids) + expect(task.wrap.lastCall.args[3]).to.equal(args) + describe "errors", -> beforeEach -> mockery.registerMock("plugins-file", ->) - @sandbox.stub(process, "on") + sinon.stub(process, "on") @err = { name: "error name" diff --git a/packages/server/test/unit/plugins/child/task_spec.coffee b/packages/server/test/unit/plugins/child/task_spec.coffee new file mode 100644 index 000000000000..12cea50ef6c6 --- /dev/null +++ b/packages/server/test/unit/plugins/child/task_spec.coffee @@ -0,0 +1,58 @@ +require("../../../spec_helper") + +EE = require('events') + +util = require("#{root}../../lib/plugins/util") +task = require("#{root}../../lib/plugins/child/task") + +describe "lib/plugins/child/task", -> + beforeEach -> + @ipc = { + send: sinon.spy() + on: sinon.stub() + removeListener: sinon.spy() + } + @events = { + "1": { + event: "task" + handler: { + "the:task": sinon.stub().returns("result") + "another:task": sinon.stub().returns("result") + "a:third:task": -> "foo" + } + } + } + @ids = {} + + sinon.stub(util, "wrapChildPromise") + + context ".getBody", -> + it "returns the stringified body of the event handler", -> + task.getBody(@ipc, @events, @ids, ["a:third:task"]) + expect(util.wrapChildPromise).to.be.called + result = util.wrapChildPromise.lastCall.args[1]("1") + expect(result.replace(/\s+/g, '')).to.equal("function(){return\"foo\";}") + + context ".getKeys", -> + it "returns the registered task keys", -> + task.getKeys(@ipc, @events, @ids) + expect(util.wrapChildPromise).to.be.called + result = util.wrapChildPromise.lastCall.args[1]("1") + expect(result).to.eql(["the:task", "another:task", "a:third:task"]) + + context ".wrap", -> + it "passes through ipc and ids", -> + task.wrap(@ipc, @events, @ids, ["the:task"]) + expect(util.wrapChildPromise).to.be.called + expect(util.wrapChildPromise.lastCall.args[0]).to.be.equal(@ipc) + expect(util.wrapChildPromise.lastCall.args[2]).to.be.equal(@ids) + + it "invokes the callback for the given task if it exists and returns the result", -> + task.wrap(@ipc, @events, @ids, ["the:task", "the:arg"]) + result = util.wrapChildPromise.lastCall.args[1]("1", ["the:arg"]) + expect(@events["1"].handler["the:task"]).to.be.calledWith("the:arg") + expect(result).to.equal("result") + + it "returns __cypress_unhandled__ if the task doesn't exist", -> + task.wrap(@ipc, @events, @ids, ["nope"]) + expect(util.wrapChildPromise.lastCall.args[1]("1")).to.equal("__cypress_unhandled__") diff --git a/packages/server/test/unit/plugins/index_spec.coffee b/packages/server/test/unit/plugins/index_spec.coffee index d7d50b5c9b08..aecd6cb4d819 100644 --- a/packages/server/test/unit/plugins/index_spec.coffee +++ b/packages/server/test/unit/plugins/index_spec.coffee @@ -10,17 +10,17 @@ describe "lib/plugins/index", -> plugins._reset() @pluginsProcess = { - send: @sandbox.spy() - on: @sandbox.stub() - kill: @sandbox.spy() + send: sinon.spy() + on: sinon.stub() + kill: sinon.spy() } - @sandbox.stub(cp, "fork").returns(@pluginsProcess) + sinon.stub(cp, "fork").returns(@pluginsProcess) @ipc = { - send: @sandbox.spy() - on: @sandbox.stub() + send: sinon.spy() + on: sinon.stub() } - @sandbox.stub(util, "wrapIpc").returns(@ipc) + sinon.stub(util, "wrapIpc").returns(@ipc) context "#init", -> it "is noop if no pluginsFile", -> @@ -33,7 +33,7 @@ describe "lib/plugins/index", -> expect(cp.fork.lastCall.args[1]).to.eql(["--file", "cypress-plugin"]) it "calls any handlers registered with the wrapped ipc", -> - handler = @sandbox.spy() + handler = sinon.spy() plugins.registerHandler(handler) plugins.init({ pluginsFile: "cypress-plugin" }) expect(handler).to.be.called @@ -65,19 +65,19 @@ describe "lib/plugins/index", -> @ipc.on.withArgs("loaded").yields(@config, [{ event: "some:event" - callbackId: 0 + eventId: 0 }]) plugins.init({ pluginsFile: "cypress-plugin" }) it "sends 'execute' message when event is executed, wrapped in promise", -> - @sandbox.stub(util, "wrapParentPromise").resolves("value").yields("00") + sinon.stub(util, "wrapParentPromise").resolves("value").yields("00") plugins.execute("some:event", "foo", "bar").then (value) => expect(util.wrapParentPromise).to.be.called expect(@ipc.send).to.be.calledWith( "execute", "some:event", - { callbackId: 0, invocationId: "00" } + { eventId: 0, invocationId: "00" } ["foo", "bar"] ) expect(value).to.equal("value") @@ -113,7 +113,7 @@ describe "lib/plugins/index", -> name: "error name" message: "error message" } - @onError = @sandbox.spy() + @onError = sinon.spy() @ipc.on.withArgs("loaded").yields([]) plugins.init({ pluginsFile: "cypress-plugin" }, { onError: @onError }) @@ -141,7 +141,7 @@ describe "lib/plugins/index", -> context "#register", -> it "registers callback for event", -> - foo = @sandbox.spy() + foo = sinon.spy() plugins.register("foo", foo) plugins.execute("foo") expect(foo).to.be.called @@ -162,7 +162,7 @@ describe "lib/plugins/index", -> context "#execute", -> it "calls the callback registered for the event", -> - foo = @sandbox.spy() + foo = sinon.spy() plugins.register("foo", foo) plugins.execute("foo", "arg1", "arg2") expect(foo).to.be.calledWith("arg1", "arg2") diff --git a/packages/server/test/unit/plugins/preprocessor_spec.coffee b/packages/server/test/unit/plugins/preprocessor_spec.coffee index 2891454b43bd..f4a3060c5797 100644 --- a/packages/server/test/unit/plugins/preprocessor_spec.coffee +++ b/packages/server/test/unit/plugins/preprocessor_spec.coffee @@ -2,7 +2,6 @@ require("../../spec_helper") EE = require("events") Fixtures = require("../../support/helpers/fixtures") -fs = require("fs-extra") path = require("path") snapshot = require("snap-shot-it") appData = require("#{root}../lib/util/app_data") @@ -22,7 +21,7 @@ describe "lib/plugins/preprocessor", -> @testPath = path.join(@todosPath, "test.coffee") @localPreprocessorPath = path.join(@todosPath, "prep.coffee") - @plugin = @sandbox.stub().returns("/path/to/output.js") + @plugin = sinon.stub().returns("/path/to/output.js") plugins.register("file:preprocessor", @plugin) preprocessor.close() @@ -48,7 +47,7 @@ describe "lib/plugins/preprocessor", -> expect(filePath).to.equal("/path/to/output.js") it "emits 'file:updated' with filePath when 'rerun' is emitted", -> - fileUpdated = @sandbox.spy() + fileUpdated = sinon.spy() preprocessor.emitter.on("file:updated", fileUpdated) preprocessor.getFile(@filePath, @config) @plugin.lastCall.args[0].emit("rerun") @@ -68,10 +67,10 @@ describe "lib/plugins/preprocessor", -> it "uses default preprocessor if none registered", -> plugins._reset() - @sandbox.stub(plugins, "register") - @sandbox.stub(plugins, "execute").returns(->) + sinon.stub(plugins, "register") + sinon.stub(plugins, "execute").returns(->) browserifyFn = -> - browserify = @sandbox.stub().returns(browserifyFn) + browserify = sinon.stub().returns(browserifyFn) mockery.registerMock("@cypress/browserify-preprocessor", browserify) preprocessor.getFile(@filePath, @config) expect(plugins.register).to.be.calledWith("file:preprocessor", browserifyFn) @@ -80,13 +79,13 @@ describe "lib/plugins/preprocessor", -> context "#removeFile", -> it "emits 'close'", -> preprocessor.getFile(@filePath, @config) - onClose = @sandbox.spy() + onClose = sinon.spy() @plugin.lastCall.args[0].on("close", onClose) preprocessor.removeFile(@filePath, @config) expect(onClose).to.be.called it "emits 'close' with file path on base emitter", -> - onClose = @sandbox.spy() + onClose = sinon.spy() preprocessor.emitter.on("close", onClose) preprocessor.getFile(@filePath, @config) preprocessor.removeFile(@filePath, @config) @@ -95,13 +94,13 @@ describe "lib/plugins/preprocessor", -> context "#close", -> it "emits 'close' on config emitter", -> preprocessor.getFile(@filePath, @config) - onClose = @sandbox.spy() + onClose = sinon.spy() @plugin.lastCall.args[0].on("close", onClose) preprocessor.close() expect(onClose).to.be.called it "emits 'close' on base emitter", -> - onClose = @sandbox.spy() + onClose = sinon.spy() preprocessor.emitter.on "close", onClose preprocessor.getFile(@filePath, @config) preprocessor.close() @@ -109,7 +108,7 @@ describe "lib/plugins/preprocessor", -> context "#clientSideError", -> beforeEach -> - @sandbox.stub(console, "error") ## keep noise out of console + sinon.stub(console, "error") ## keep noise out of console it "send javascript string with the error", -> expect(preprocessor.clientSideError("an error")).to.equal(""" diff --git a/packages/server/test/unit/plugins/util_spec.coffee b/packages/server/test/unit/plugins/util_spec.coffee index b4746eda5c8c..268128fb0171 100644 --- a/packages/server/test/unit/plugins/util_spec.coffee +++ b/packages/server/test/unit/plugins/util_spec.coffee @@ -9,8 +9,8 @@ describe "lib/plugins/util", -> context "#wrapIpc", -> beforeEach -> @theProcess = { - send: @sandbox.spy() - on: @sandbox.stub() + send: sinon.spy() + on: sinon.stub() } @ipc = util.wrapIpc(@theProcess) @@ -28,7 +28,7 @@ describe "lib/plugins/util", -> expect(@theProcess.send).not.to.be.called it "#on listens for process messages that match event", -> - handler = @sandbox.spy() + handler = sinon.spy() @ipc.on("event-name", handler) @theProcess.on.yield({ event: "event-name" @@ -37,7 +37,7 @@ describe "lib/plugins/util", -> expect(handler).to.be.calledWith("arg1", "arg2") it "#removeListener emoves handler", -> - handler = @sandbox.spy() + handler = sinon.spy() @ipc.on("event-name", handler) @ipc.removeListener("event-name", handler) @theProcess.on.yield({ @@ -49,13 +49,13 @@ describe "lib/plugins/util", -> context "#wrapChildPromise", -> beforeEach -> @ipc = { - send: @sandbox.spy() - on: @sandbox.stub() - removeListener: @sandbox.spy() + send: sinon.spy() + on: sinon.stub() + removeListener: sinon.spy() } - @invoke = @sandbox.stub() + @invoke = sinon.stub() @ids = { - callbackId: 0 + eventId: 0 invocationId: "00" } @args = [] @@ -73,6 +73,11 @@ describe "lib/plugins/util", -> util.wrapChildPromise(@ipc, @invoke, @ids).then => expect(@ipc.send).to.be.calledWith("promise:fulfilled:00", null, "value") + it "serializes undefined", -> + @invoke.resolves(undefined) + util.wrapChildPromise(@ipc, @invoke, @ids).then => + expect(@ipc.send).to.be.calledWith("promise:fulfilled:00", null, "__cypress_undefined__") + it "sends 'promise:fulfilled:{invocatationId}' with error when promise rejects", -> err = new Error("fail") err.code = "ERM_DUN_FAILED" @@ -90,11 +95,11 @@ describe "lib/plugins/util", -> context "#wrapParentPromise", -> beforeEach -> @ipc = { - send: @sandbox.spy() - on: @sandbox.stub() - removeListener: @sandbox.spy() + send: sinon.spy() + on: sinon.stub() + removeListener: sinon.spy() } - @callback = @sandbox.spy() + @callback = sinon.spy() it "returns a promise", -> expect(util.wrapParentPromise(@ipc, 0, @callback)).to.be.an.instanceOf(Promise) @@ -106,6 +111,13 @@ describe "lib/plugins/util", -> promise.then (value) -> expect(value).to.equal("value") + it "deserializes undefined", -> + promise = util.wrapParentPromise(@ipc, 0, @callback) + invocationId = @callback.lastCall.args[0] + @ipc.on.withArgs("promise:fulfilled:#{invocationId}").yield(null, "__cypress_undefined__") + promise.then (value) -> + expect(value).to.equal(undefined) + it "rejects the promise when 'promise:fulfilled:{invocationId}' event is received with error", -> promise = util.wrapParentPromise(@ipc, 0, @callback) invocationId = @callback.lastCall.args[0] diff --git a/packages/server/test/unit/project_spec.coffee b/packages/server/test/unit/project_spec.coffee index e8d067d4afad..9b7f563e2de6 100644 --- a/packages/server/test/unit/project_spec.coffee +++ b/packages/server/test/unit/project_spec.coffee @@ -2,9 +2,8 @@ require("../spec_helper") path = require("path") Promise = require("bluebird") -fs = require("fs-extra") +commitInfo = require("@cypress/commit-info") Fixtures = require("../support/helpers/fixtures") -ids = require("#{root}lib/ids") api = require("#{root}lib/api") user = require("#{root}lib/user") cache = require("#{root}lib/cache") @@ -14,11 +13,11 @@ scaffold = require("#{root}lib/scaffold") Server = require("#{root}lib/server") Project = require("#{root}lib/project") Automation = require("#{root}lib/automation") -settings = require("#{root}lib/util/settings") savedState = require("#{root}lib/saved_state") -commitInfo = require("@cypress/commit-info") preprocessor = require("#{root}lib/plugins/preprocessor") plugins = require("#{root}lib/plugins") +fs = require("#{root}lib/util/fs") +settings = require("#{root}lib/util/settings") describe "lib/project", -> beforeEach -> @@ -51,8 +50,8 @@ describe "lib/project", -> context "#saveState", -> beforeEach -> integrationFolder = "the/save/state/test" - @sandbox.stub(config, "get").withArgs(@todosPath).resolves({ integrationFolder }) - @sandbox.stub(@project, "determineIsNewProject").withArgs(integrationFolder).resolves(false) + sinon.stub(config, "get").withArgs(@todosPath).resolves({ integrationFolder }) + sinon.stub(@project, "determineIsNewProject").withArgs(integrationFolder).resolves(false) @project.cfg = { integrationFolder } savedState(@project.projectRoot) .then (state) -> state.remove() @@ -89,13 +88,13 @@ describe "lib/project", -> context "#getConfig", -> integrationFolder = "foo/bar/baz" beforeEach -> - @sandbox.stub(config, "get").withArgs(@todosPath, {foo: "bar"}).resolves({ baz: "quux", integrationFolder }) - @sandbox.stub(@project, "determineIsNewProject").withArgs(integrationFolder).resolves(false) + sinon.stub(config, "get").withArgs(@todosPath, {foo: "bar"}).resolves({ baz: "quux", integrationFolder }) + sinon.stub(@project, "determineIsNewProject").withArgs(integrationFolder).resolves(false) it "calls config.get with projectRoot + options + saved state", -> savedState(@todosPath) .then (state) => - @sandbox.stub(state, "get").resolves({ reporterWidth: 225 }) + sinon.stub(state, "get").resolves({ reporterWidth: 225 }) @project.getConfig({foo: "bar"}) .then (cfg) -> expect(cfg).to.deep.eq({ @@ -123,7 +122,7 @@ describe "lib/project", -> it "sets cfg.isNewProject to true when state.showedOnBoardingModal is true", -> savedState(@todosPath) .then (state) => - @sandbox.stub(state, "get").resolves({ showedOnBoardingModal: true }) + sinon.stub(state, "get").resolves({ showedOnBoardingModal: true }) @project.getConfig({foo: "bar"}) .then (cfg) -> @@ -138,15 +137,15 @@ describe "lib/project", -> context "#open", -> beforeEach -> - @sandbox.stub(@project, "watchSettingsAndStartWebsockets").resolves() - @sandbox.stub(@project, "checkSupportFile").resolves() - @sandbox.stub(@project, "scaffold").resolves() - @sandbox.stub(@project, "getConfig").resolves(@config) - @sandbox.stub(Server.prototype, "open").resolves([]) - @sandbox.stub(Server.prototype, "reset") - @sandbox.stub(config, "updateWithPluginValues").returns(@config) - @sandbox.stub(scaffold, "plugins").resolves() - @sandbox.stub(plugins, "init").resolves() + sinon.stub(@project, "watchSettingsAndStartWebsockets").resolves() + sinon.stub(@project, "checkSupportFile").resolves() + sinon.stub(@project, "scaffold").resolves() + sinon.stub(@project, "getConfig").resolves(@config) + sinon.stub(Server.prototype, "open").resolves([]) + sinon.stub(Server.prototype, "reset") + sinon.stub(config, "updateWithPluginValues").returns(@config) + sinon.stub(scaffold, "plugins").resolves() + sinon.stub(plugins, "init").resolves() it "calls #watchSettingsAndStartWebsockets with options + config", -> opts = {changeEvents: false, onAutomationRequest: ->} @@ -176,7 +175,7 @@ describe "lib/project", -> expect(scaffold.plugins).to.be.calledWith(path.dirname(@config.pluginsFile)) it "calls options.onError with plugins error when there is a plugins error", -> - onError = @sandbox.spy() + onError = sinon.spy() err = { name: "plugin error name" message: "plugin error message" @@ -188,7 +187,7 @@ describe "lib/project", -> expect(onError).to.be.calledWith(err) it "updates config.state when saved state changes", -> - @sandbox.spy(@project, "saveState") + sinon.spy(@project, "saveState") options = {} @@ -217,17 +216,17 @@ describe "lib/project", -> beforeEach -> @project = Project("/_test-output/path/to/project") - @sandbox.stub(@project, "getConfig").resolves(@config) - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(@project, "getConfig").resolves(@config) + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") it "closes server", -> - @project.server = @sandbox.stub({close: ->}) + @project.server = sinon.stub({close: ->}) @project.close().then => expect(@project.server.close).to.be.calledOnce it "closes watchers", -> - @project.watchers = @sandbox.stub({close: ->}) + @project.watchers = sinon.stub({close: ->}) @project.close().then => expect(@project.watchers.close).to.be.calledOnce @@ -238,9 +237,9 @@ describe "lib/project", -> context "#getRuns", -> beforeEach -> @project = Project(@todosPath) - @sandbox.stub(settings, "read").resolves({projectId: "id-123"}) - @sandbox.stub(api, "getProjectRuns").resolves('runs') - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(settings, "read").resolves({projectId: "id-123"}) + sinon.stub(api, "getProjectRuns").resolves('runs') + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") it "calls api.getProjectRuns with project id + session", -> @project.getRuns().then (runs) -> @@ -250,10 +249,10 @@ describe "lib/project", -> context "#scaffold", -> beforeEach -> @project = Project("/_test-output/path/to/project") - @sandbox.stub(scaffold, "integration").resolves() - @sandbox.stub(scaffold, "fixture").resolves() - @sandbox.stub(scaffold, "support").resolves() - @sandbox.stub(scaffold, "plugins").resolves() + sinon.stub(scaffold, "integration").resolves() + sinon.stub(scaffold, "fixture").resolves() + sinon.stub(scaffold, "support").resolves() + sinon.stub(scaffold, "plugins").resolves() @obj = {projectRoot: "pr", fixturesFolder: "ff", integrationFolder: "if", supportFolder: "sf", pluginsFile: "pf/index.js"} @@ -278,7 +277,15 @@ describe "lib/project", -> beforeEach -> @project = Project("/_test-output/path/to/project") @project.server = {startWebsockets: ->} - @watch = @sandbox.stub(@project.watchers, "watch") + sinon.stub(settings, "pathToCypressJson").returns("/path/to/cypress.json") + sinon.stub(settings, "pathToCypressEnvJson").returns("/path/to/cypress.env.json") + @watch = sinon.stub(@project.watchers, "watch") + + it "watches cypress.json and cypress.env.json", -> + @project.watchSettingsAndStartWebsockets({onSettingsChanged: ->}) + expect(@watch).to.be.calledTwice + expect(@watch).to.be.calledWith("/path/to/cypress.json") + expect(@watch).to.be.calledWith("/path/to/cypress.env.json") it "sets onChange event when {changeEvents: true}", (done) -> @project.watchSettingsAndStartWebsockets({onSettingsChanged: done}) @@ -297,9 +304,9 @@ describe "lib/project", -> it "does not call onSettingsChanged when generatedProjectIdTimestamp is less than 1 second", -> @project.generatedProjectIdTimestamp = timestamp = new Date() - emit = @sandbox.spy(@project, "emit") + emit = sinon.spy(@project, "emit") - stub = @sandbox.stub() + stub = sinon.stub() @project.watchSettingsAndStartWebsockets({onSettingsChanged: stub}) @@ -318,10 +325,10 @@ describe "lib/project", -> context "#checkSupportFile", -> beforeEach -> - @sandbox.stub(fs, "pathExists").resolves(true) + sinon.stub(fs, "pathExists").resolves(true) @project = Project("/_test-output/path/to/project") - @project.server = {onTestFileChange: @sandbox.spy()} - @sandbox.stub(preprocessor, "getFile").resolves() + @project.server = {onTestFileChange: sinon.spy()} + sinon.stub(preprocessor, "getFile").resolves() @config = { projectRoot: "/path/to/root/" supportFile: "/path/to/root/foo/bar.js" @@ -340,10 +347,10 @@ describe "lib/project", -> context "#watchPluginsFile", -> beforeEach -> - @sandbox.stub(fs, "pathExists").resolves(true) + sinon.stub(fs, "pathExists").resolves(true) @project = Project("/_test-output/path/to/project") - @project.watchers = { watch: @sandbox.spy() } - @sandbox.stub(plugins, "init").resolves() + @project.watchers = { watchTree: sinon.spy() } + sinon.stub(plugins, "init").resolves() @config = { pluginsFile: "/path/to/plugins-file" } @@ -351,22 +358,22 @@ describe "lib/project", -> it "does nothing when {pluginsFile: false}", -> @config.pluginsFile = false @project.watchPluginsFile(@config).then => - expect(@project.watchers.watch).not.to.be.called + expect(@project.watchers.watchTree).not.to.be.called it "does nothing if pluginsFile does not exist", -> fs.pathExists.resolves(false) @project.watchPluginsFile(@config).then => - expect(@project.watchers.watch).not.to.be.called + expect(@project.watchers.watchTree).not.to.be.called it "watches the pluginsFile", -> @project.watchPluginsFile(@config).then => - expect(@project.watchers.watch).to.be.calledWith(@config.pluginsFile) - expect(@project.watchers.watch.lastCall.args[1]).to.be.an("object") - expect(@project.watchers.watch.lastCall.args[1].onChange).to.be.a("function") + expect(@project.watchers.watchTree).to.be.calledWith(@config.pluginsFile) + expect(@project.watchers.watchTree.lastCall.args[1]).to.be.an("object") + expect(@project.watchers.watchTree.lastCall.args[1].onChange).to.be.a("function") it "calls plugins.init when file changes", -> @project.watchPluginsFile(@config).then => - @project.watchers.watch.firstCall.args[1].onChange() + @project.watchers.watchTree.firstCall.args[1].onChange() expect(plugins.init).to.be.calledWith(@config) it "handles errors from calling plugins.init", (done) -> @@ -378,16 +385,16 @@ describe "lib/project", -> done() }) .then => - @project.watchers.watch.firstCall.args[1].onChange() + @project.watchers.watchTree.firstCall.args[1].onChange() return context "#watchSettingsAndStartWebsockets", -> beforeEach -> @project = Project("/_test-output/path/to/project") @project.watchers = {} - @project.server = @sandbox.stub({startWebsockets: ->}) - @sandbox.stub(@project, "watchSettings") - @sandbox.stub(Automation, "create").returns("automation") + @project.server = sinon.stub({startWebsockets: ->}) + sinon.stub(@project, "watchSettings") + sinon.stub(Automation, "create").returns("automation") it "calls server.startWebsockets with automation + config", -> c = {} @@ -397,7 +404,7 @@ describe "lib/project", -> expect(@project.server.startWebsockets).to.be.calledWith("automation", c) it "passes onReloadBrowser callback", -> - fn = @sandbox.stub() + fn = sinon.stub() @project.server.startWebsockets.yieldsTo("onReloadBrowser") @@ -406,35 +413,26 @@ describe "lib/project", -> expect(fn).to.be.calledOnce context "#getProjectId", -> - afterEach -> - delete process.env.CYPRESS_PROJECT_ID - beforeEach -> @project = Project("/_test-output/path/to/project") - @verifyExistence = @sandbox.stub(Project.prototype, "verifyExistence").resolves() - - it "resolves with process.env.CYPRESS_PROJECT_ID if set", -> - process.env.CYPRESS_PROJECT_ID = "123" - - @project.getProjectId().then (id) -> - expect(id).to.eq("123") + @verifyExistence = sinon.stub(Project.prototype, "verifyExistence").resolves() it "calls verifyExistence", -> - @sandbox.stub(settings, "read").resolves({projectId: "id-123"}) + sinon.stub(settings, "read").resolves({projectId: "id-123"}) @project.getProjectId() .then => expect(@verifyExistence).to.be.calledOnce it "returns the project id from settings", -> - @sandbox.stub(settings, "read").resolves({projectId: "id-123"}) + sinon.stub(settings, "read").resolves({projectId: "id-123"}) @project.getProjectId() .then (id) -> expect(id).to.eq "id-123" it "throws NO_PROJECT_ID with the projectRoot when no projectId was found", -> - @sandbox.stub(settings, "read").resolves({}) + sinon.stub(settings, "read").resolves({}) @project.getProjectId() .then (id) -> @@ -447,7 +445,7 @@ describe "lib/project", -> err = new Error() err.code = "EACCES" - @sandbox.stub(settings, "read").rejects(err) + sinon.stub(settings, "read").rejects(err) @project.getProjectId() .then (id) -> @@ -459,7 +457,7 @@ describe "lib/project", -> beforeEach -> @project = Project("/_test-output/path/to/project") - @sandbox.stub(settings, "write") + sinon.stub(settings, "write") .withArgs(@project.projectRoot, {projectId: "id-123"}) .resolves({projectId: "id-123"}) @@ -471,65 +469,33 @@ describe "lib/project", -> @project.writeProjectId("id-123").then => expect(@project.generatedProjectIdTimestamp).to.be.a("date") - context "#ensureSpecUrl", -> + context "#getSpecUrl", -> beforeEach -> @project2 = Project(@idsPath) settings.write(@idsPath, {port: 2020}) it "returns fully qualified url when spec exists", -> - @project2.ensureSpecUrl("cypress/integration/bar.js") + @project2.getSpecUrl("cypress/integration/bar.js") .then (str) -> expect(str).to.eq("http://localhost:2020/__/#/tests/integration/bar.js") it "returns fully qualified url on absolute path to spec", -> todosSpec = path.join(@todosPath, "tests/sub/sub_test.coffee") - @project.ensureSpecUrl(todosSpec) + @project.getSpecUrl(todosSpec) .then (str) -> expect(str).to.eq("http://localhost:8888/__/#/tests/integration/sub/sub_test.coffee") it "returns __all spec url", -> - @project.ensureSpecUrl() + @project.getSpecUrl() .then (str) -> expect(str).to.eq("http://localhost:8888/__/#/tests/__all") it "returns __all spec url with spec is __all", -> - @project.ensureSpecUrl('__all') + @project.getSpecUrl('__all') .then (str) -> expect(str).to.eq("http://localhost:8888/__/#/tests/__all") - it "throws when spec isnt found", -> - @project.ensureSpecUrl("does/not/exist.js") - .catch (err) -> - expect(err.type).to.eq("SPEC_FILE_NOT_FOUND") - - context "#ensureSpecExists", -> - beforeEach -> - @project2 = Project(@idsPath) - - it "resolves relative path to test file against projectRoot", -> - @project2.ensureSpecExists("cypress/integration/foo.coffee") - .then => - @project.ensureSpecExists("tests/test1.js") - - it "resolves + returns absolute path to test file", -> - idsSpec = path.join(@idsPath, "cypress/integration/foo.coffee") - todosSpec = path.join(@todosPath, "tests/sub/sub_test.coffee") - - @project2.ensureSpecExists(idsSpec) - .then (spec1) => - expect(spec1).to.eq(idsSpec) - - @project.ensureSpecExists(todosSpec) - .then (spec2) -> - expect(spec2).to.eq(todosSpec) - - it "throws SPEC_FILE_NOT_FOUND when spec does not exist", -> - @project2.ensureSpecExists("does/not/exist.js") - .catch (err) => - expect(err.type).to.eq("SPEC_FILE_NOT_FOUND") - expect(err.message).to.include(path.join(@idsPath, "does/not/exist.js")) - context ".add", -> beforeEach -> @pristinePath = Fixtures.projectPath("pristine") @@ -543,7 +509,7 @@ describe "lib/project", -> describe "if project at path has id", -> it "returns object containing path and id", -> - @sandbox.stub(settings, "read").resolves({projectId: "id-123"}) + sinon.stub(settings, "read").resolves({projectId: "id-123"}) Project.add(@pristinePath) .then (project) => @@ -552,7 +518,7 @@ describe "lib/project", -> describe "if project at path does not have id", -> it "returns object containing just the path", -> - @sandbox.stub(settings, "read").rejects() + sinon.stub(settings, "read").rejects() Project.add(@pristinePath) .then (project) => @@ -564,10 +530,10 @@ describe "lib/project", -> @project = Project("/_test-output/path/to/project") @newProject = { id: "project-id-123" } - @sandbox.stub(@project, "writeProjectId").resolves("project-id-123") - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") - @sandbox.stub(commitInfo, "getRemoteOrigin").resolves("remoteOrigin") - @sandbox.stub(api, "createProject") + sinon.stub(@project, "writeProjectId").resolves("project-id-123") + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(commitInfo, "getRemoteOrigin").resolves("remoteOrigin") + sinon.stub(api, "createProject") .withArgs({foo: "bar"}, "remoteOrigin", "auth-token-123") .resolves(@newProject) @@ -587,9 +553,9 @@ describe "lib/project", -> beforeEach -> @recordKeys = [] @project = Project(@pristinePath) - @sandbox.stub(settings, "read").resolves({projectId: "id-123"}) - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") - @sandbox.stub(api, "getProjectRecordKeys").resolves(@recordKeys) + sinon.stub(settings, "read").resolves({projectId: "id-123"}) + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(api, "getProjectRecordKeys").resolves(@recordKeys) it "calls api.getProjectRecordKeys with project id + session", -> @project.getRecordKeys().then -> @@ -602,8 +568,8 @@ describe "lib/project", -> context "#requestAccess", -> beforeEach -> @project = Project(@pristinePath) - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") - @sandbox.stub(api, "requestAccess").resolves("response") + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(api, "requestAccess").resolves("response") it "calls api.requestAccess with project id + auth token", -> @project.requestAccess("project-id-123").then -> @@ -615,7 +581,7 @@ describe "lib/project", -> context ".remove", -> beforeEach -> - @sandbox.stub(cache, "removeProject").resolves() + sinon.stub(cache, "removeProject").resolves() it "calls cache.removeProject with path", -> Project.remove("/_test-output/path/to/project").then -> @@ -628,8 +594,8 @@ describe "lib/project", -> context ".getOrgs", -> beforeEach -> - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") - @sandbox.stub(api, "getOrgs").resolves([]) + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(api, "getOrgs").resolves([]) it "calls api.getOrgs", -> Project.getOrgs().then (orgs) -> @@ -639,20 +605,20 @@ describe "lib/project", -> context ".paths", -> beforeEach -> - @sandbox.stub(cache, "getProjectPaths").resolves([]) + sinon.stub(cache, "getProjectRoots").resolves([]) - it "calls cache.getProjectPaths", -> + it "calls cache.getProjectRoots", -> Project.paths().then (ret) -> expect(ret).to.deep.eq([]) - expect(cache.getProjectPaths).to.be.calledOnce + expect(cache.getProjectRoots).to.be.calledOnce context ".getPathsAndIds", -> beforeEach -> - @sandbox.stub(cache, "getProjectPaths").resolves([ + sinon.stub(cache, "getProjectRoots").resolves([ "/path/to/first" "/path/to/second" ]) - @sandbox.stub(settings, "id").resolves("id-123") + sinon.stub(settings, "id").resolves("id-123") it "returns array of objects with paths and ids", -> Project.getPathsAndIds().then (pathsAndIds) -> @@ -669,38 +635,38 @@ describe "lib/project", -> context ".getProjectStatuses", -> beforeEach -> - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") it "gets projects from api", -> - @sandbox.stub(api, "getProjects").resolves([]) + sinon.stub(api, "getProjects").resolves([]) Project.getProjectStatuses([]) .then -> expect(api.getProjects).to.have.been.calledWith("auth-token-123") it "returns array of projects", -> - @sandbox.stub(api, "getProjects").resolves([]) + sinon.stub(api, "getProjects").resolves([]) Project.getProjectStatuses([]) .then (projectsWithStatuses) => expect(projectsWithStatuses).to.eql([]) it "returns same number as client projects, even if there are less api projects", -> - @sandbox.stub(api, "getProjects").resolves([]) + sinon.stub(api, "getProjects").resolves([]) Project.getProjectStatuses([{}]) .then (projectsWithStatuses) => expect(projectsWithStatuses.length).to.eql(1) it "returns same number as client projects, even if there are more api projects", -> - @sandbox.stub(api, "getProjects").resolves([{}, {}]) + sinon.stub(api, "getProjects").resolves([{}, {}]) Project.getProjectStatuses([{}]) .then (projectsWithStatuses) => expect(projectsWithStatuses.length).to.eql(1) it "merges in details of matching projects", -> - @sandbox.stub(api, "getProjects").resolves([ + sinon.stub(api, "getProjects").resolves([ { id: "id-123", lastBuildStatus: "passing" } ]) @@ -714,7 +680,7 @@ describe "lib/project", -> }) it "returns client project when it has no id", -> - @sandbox.stub(api, "getProjects").resolves([]) + sinon.stub(api, "getProjects").resolves([]) Project.getProjectStatuses([{ path: "/_test-output/path/to/project" }]) .then (projectsWithStatuses) => @@ -725,10 +691,10 @@ describe "lib/project", -> describe "when client project has id and there is no matching user project", -> beforeEach -> - @sandbox.stub(api, "getProjects").resolves([]) + sinon.stub(api, "getProjects").resolves([]) it "marks project as invalid if api 404s", -> - @sandbox.stub(api, "getProject").rejects({name: "", message: "", statusCode: 404}) + sinon.stub(api, "getProject").rejects({name: "", message: "", statusCode: 404}) Project.getProjectStatuses([{ id: "id-123", path: "/_test-output/path/to/project" }]) .then (projectsWithStatuses) => @@ -739,7 +705,7 @@ describe "lib/project", -> }) it "marks project as unauthorized if api 403s", -> - @sandbox.stub(api, "getProject").rejects({name: "", message: "", statusCode: 403}) + sinon.stub(api, "getProject").rejects({name: "", message: "", statusCode: 403}) Project.getProjectStatuses([{ id: "id-123", path: "/_test-output/path/to/project" }]) .then (projectsWithStatuses) => @@ -750,7 +716,7 @@ describe "lib/project", -> }) it "merges in project details and marks valid if somehow project exists and is authorized", -> - @sandbox.stub(api, "getProject").resolves({ id: "id-123", lastBuildStatus: "passing" }) + sinon.stub(api, "getProject").resolves({ id: "id-123", lastBuildStatus: "passing" }) Project.getProjectStatuses([{ id: "id-123", path: "/_test-output/path/to/project" }]) .then (projectsWithStatuses) => @@ -763,11 +729,11 @@ describe "lib/project", -> it "throws error if not accounted for", -> error = {name: "", message: ""} - @sandbox.stub(api, "getProject").rejects(error) + sinon.stub(api, "getProject").rejects(error) Project.getProjectStatuses([{ id: "id-123", path: "/_test-output/path/to/project" }]) .then => - throw new Error("Should throw error") + throw new Error("should have caught error but did not") .catch (err) -> expect(err).to.equal(error) @@ -777,17 +743,17 @@ describe "lib/project", -> id: "id-123", path: "/_test-output/path/to/project" } - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") it "gets project from api", -> - @sandbox.stub(api, "getProject").resolves([]) + sinon.stub(api, "getProject").resolves([]) Project.getProjectStatus(@clientProject) .then -> expect(api.getProject).to.have.been.calledWith("id-123", "auth-token-123") it "returns project merged with details", -> - @sandbox.stub(api, "getProject").resolves({ + sinon.stub(api, "getProject").resolves({ lastBuildStatus: "passing" }) @@ -801,7 +767,7 @@ describe "lib/project", -> }) it "returns project, marked as valid, if it does not have an id, without querying api", -> - @sandbox.stub(api, "getProject") + sinon.stub(api, "getProject") @clientProject.id = undefined Project.getProjectStatus(@clientProject) @@ -814,7 +780,7 @@ describe "lib/project", -> expect(api.getProject).not.to.be.called it "marks project as invalid if api 404s", -> - @sandbox.stub(api, "getProject").rejects({name: "", message: "", statusCode: 404}) + sinon.stub(api, "getProject").rejects({name: "", message: "", statusCode: 404}) Project.getProjectStatus(@clientProject) .then (project) => @@ -825,7 +791,7 @@ describe "lib/project", -> }) it "marks project as unauthorized if api 403s", -> - @sandbox.stub(api, "getProject").rejects({name: "", message: "", statusCode: 403}) + sinon.stub(api, "getProject").rejects({name: "", message: "", statusCode: 403}) Project.getProjectStatus(@clientProject) .then (project) => @@ -837,30 +803,20 @@ describe "lib/project", -> it "throws error if not accounted for", -> error = {name: "", message: ""} - @sandbox.stub(api, "getProject").rejects(error) + sinon.stub(api, "getProject").rejects(error) Project.getProjectStatus(@clientProject) .then => - throw new Error("Should throw error") + throw new Error("should have caught error but did not") .catch (err) -> expect(err).to.equal(error) - context ".removeIds", -> - beforeEach -> - @sandbox.stub(ids, "remove").resolves({}) - - it "calls id.remove with path to project tests", -> - p = Fixtures.projectPath("ids") - - Project.removeIds(p).then -> - expect(ids.remove).to.be.calledWith(p + "/cypress/integration") - context ".getSecretKeyByPath", -> beforeEach -> - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") it "calls api.getProjectToken with id + session", -> - @sandbox.stub(api, "getProjectToken") + sinon.stub(api, "getProjectToken") .withArgs(@projectId, "auth-token-123") .resolves("key-123") @@ -868,7 +824,7 @@ describe "lib/project", -> expect(key).to.eq("key-123") it "throws CANNOT_FETCH_PROJECT_TOKEN on error", -> - @sandbox.stub(api, "getProjectToken") + sinon.stub(api, "getProjectToken") .withArgs(@projectId, "auth-token-123") .rejects(new Error()) @@ -880,10 +836,10 @@ describe "lib/project", -> context ".generateSecretKeyByPath", -> beforeEach -> - @sandbox.stub(user, "ensureAuthToken").resolves("auth-token-123") + sinon.stub(user, "ensureAuthToken").resolves("auth-token-123") it "calls api.updateProjectToken with id + session", -> - @sandbox.stub(api, "updateProjectToken") + sinon.stub(api, "updateProjectToken") .withArgs(@projectId, "auth-token-123") .resolves("new-key-123") @@ -891,7 +847,7 @@ describe "lib/project", -> expect(key).to.eq("new-key-123") it "throws CANNOT_CREATE_PROJECT_TOKEN on error", -> - @sandbox.stub(api, "updateProjectToken") + sinon.stub(api, "updateProjectToken") .withArgs(@projectId, "auth-token-123") .rejects(new Error()) @@ -900,22 +856,3 @@ describe "lib/project", -> throw new Error("should have caught error but did not") .catch (err) -> expect(err.type).to.eq("CANNOT_CREATE_PROJECT_TOKEN") - - context ".findSpecs", -> - it "returns all the specs without a specPattern", -> - Project.findSpecs(@todosPath) - .then (specs = []) -> - expect(specs).to.deep.eq([ - "etc/etc.js" - "sub/sub_test.coffee" - "test1.js" - "test2.coffee" - ]) - - it "returns glob subset matching specPattern", -> - Project.findSpecs(@todosPath, "tests/*") - .then (specs = []) -> - expect(specs).to.deep.eq([ - "test1.js" - "test2.coffee" - ]) diff --git a/packages/server/test/unit/proxy_spec.coffee b/packages/server/test/unit/proxy_spec.coffee index 8edef57c77df..875a618838ef 100644 --- a/packages/server/test/unit/proxy_spec.coffee +++ b/packages/server/test/unit/proxy_spec.coffee @@ -120,7 +120,7 @@ describe "lib/proxy", -> # context "relative files", -> # it "#getRelativeFileContent strips trailing slashes", -> -# createReadStream = @sandbox.stub(fs, "createReadStream") +# createReadStream = sinon.stub(fs, "createReadStream") # proxy.getRelativeFileContent("index.html/", {}) # expect(createReadStream).to.be.calledWith("/Users/brian/app/index.html") diff --git a/packages/server/test/unit/random_spec.coffee b/packages/server/test/unit/random_spec.coffee new file mode 100644 index 000000000000..7bb28fac0c32 --- /dev/null +++ b/packages/server/test/unit/random_spec.coffee @@ -0,0 +1,16 @@ +require("../spec_helper") + +randomstring = require("randomstring") +random = require("#{root}lib/util/random") + +context ".id", -> + it "returns random.generate string", -> + sinon.spy(randomstring, "generate") + + id = random.id() + expect(id.length).to.eq(5) + + expect(randomstring.generate).to.be.calledWith({ + length: 5 + capitalization: "lowercase" + }) diff --git a/packages/server/test/unit/reporter_spec.coffee b/packages/server/test/unit/reporter_spec.coffee index b91bc13a95a4..02bf65c2b3ef 100644 --- a/packages/server/test/unit/reporter_spec.coffee +++ b/packages/server/test/unit/reporter_spec.coffee @@ -58,7 +58,7 @@ describe "lib/reporter", -> context ".create", -> it "can create mocha-teamcity-reporter", -> - teamCityFn = @sandbox.stub() + teamCityFn = sinon.stub() mockery.registerMock("@cypress/mocha-teamcity-reporter", teamCityFn) reporter = Reporter.create("teamcity") @@ -68,7 +68,7 @@ describe "lib/reporter", -> expect(teamCityFn).to.be.calledWith(reporter.runner) it "can create mocha-junit-reporter", -> - junitFn = @sandbox.stub() + junitFn = sinon.stub() mockery.registerMock("mocha-junit-reporter", junitFn) reporter = Reporter.create("junit") @@ -97,8 +97,8 @@ describe "lib/reporter", -> expect(args[1].fullTitle()).to.eq title context "#stats", -> - it "has reporterName and failingTests in stats", -> - @sandbox.stub(Date, "now").returns(1234) + it "has reporterName stats, reporterStats, etc", -> + sinon.stub(Date, "now").returns(1234) @reporter.emit("test", @testObj) @reporter.emit("fail", @testObj) @@ -106,11 +106,11 @@ describe "lib/reporter", -> @reporter.reporterName = "foo" - snapshot(@reporter.stats()) + snapshot(@reporter.results()) context "#emit", -> beforeEach -> - @emit = @sandbox.spy @reporter.runner, "emit" + @emit = sinon.spy @reporter.runner, "emit" it "emits start", -> @reporter.emit("start") diff --git a/packages/server/test/unit/request_spec.coffee b/packages/server/test/unit/request_spec.coffee index 74051a320b77..c1e11bee146d 100644 --- a/packages/server/test/unit/request_spec.coffee +++ b/packages/server/test/unit/request_spec.coffee @@ -29,7 +29,7 @@ describe "lib/request", -> context "#normalizeResponse", -> beforeEach -> - @push = @sandbox.stub() + @push = sinon.stub() it "sets status to statusCode and deletes statusCode", -> expect(request.normalizeResponse(@push, { @@ -74,10 +74,10 @@ describe "lib/request", -> context "#send", -> beforeEach -> - @fn = @sandbox.stub() + @fn = sinon.stub() it "sets strictSSL=false", -> - init = @sandbox.spy(request.rp.Request.prototype, "init") + init = sinon.spy(request.rp.Request.prototype, "init") nock("http://www.github.com") .get("/foo") @@ -271,6 +271,7 @@ describe "lib/request", -> it "sets duration on response", -> nock("http://localhost:8080") .get("/foo") + .delay(10) .reply(200, "123", { "Content-Type": "text/plain" }) @@ -496,7 +497,7 @@ describe "lib/request", -> expect(resp.body).to.eq("") it "does not send body", -> - init = @sandbox.spy(request.rp.Request.prototype, "init") + init = sinon.spy(request.rp.Request.prototype, "init") body = { foo: "bar" @@ -517,7 +518,7 @@ describe "lib/request", -> expect(init).not.to.be.calledWithMatch({body: body}) it "does not set json=true", -> - init = @sandbox.spy(request.rp.Request.prototype, "init") + init = sinon.spy(request.rp.Request.prototype, "init") request.send({}, @fn, { url: "http://localhost:8080/login" diff --git a/packages/server/test/unit/routes_util_spec.coffee b/packages/server/test/unit/routes_util_spec.coffee index d7efb08b9b7c..4aa3efb37981 100644 --- a/packages/server/test/unit/routes_util_spec.coffee +++ b/packages/server/test/unit/routes_util_spec.coffee @@ -1,43 +1,46 @@ require("../spec_helper") -Routes = require("#{root}/lib/util/routes") +routes = require("#{root}/lib/util/routes") describe "lib/util/routes", -> it "api", -> - expect(Routes.api()).to.eq "http://localhost:1234/" + expect(routes.api()).to.eq "http://localhost:1234/" it "auth", -> - expect(Routes.auth()).to.eq "http://localhost:1234/auth" + expect(routes.auth()).to.eq "http://localhost:1234/auth" it "ping", -> - expect(Routes.ping()).to.eq("http://localhost:1234/ping") + expect(routes.ping()).to.eq("http://localhost:1234/ping") it "signin", -> - expect(Routes.signin()).to.eq "http://localhost:1234/signin" + expect(routes.signin()).to.eq "http://localhost:1234/signin" it "signin?code=abc", -> - expect(Routes.signin({code: "abc"})).to.eq "http://localhost:1234/signin?code=abc" + expect(routes.signin({code: "abc"})).to.eq "http://localhost:1234/signin?code=abc" it "signout", -> - expect(Routes.signout()).to.eq "http://localhost:1234/signout" + expect(routes.signout()).to.eq "http://localhost:1234/signout" it "runs", -> - expect(Routes.runs()).to.eq("http://localhost:1234/builds") + expect(routes.runs()).to.eq("http://localhost:1234/runs") it "instances", -> - expect(Routes.instances(123)).to.eq("http://localhost:1234/builds/123/instances") + expect(routes.instances(123)).to.eq("http://localhost:1234/runs/123/instances") it "instance", -> - expect(Routes.instance(123)).to.eq("http://localhost:1234/instances/123") + expect(routes.instance(123)).to.eq("http://localhost:1234/instances/123") it "projects", -> - expect(Routes.projects()).to.eq "http://localhost:1234/projects" + expect(routes.projects()).to.eq "http://localhost:1234/projects" it "project", -> - expect(Routes.project("123-foo")).to.eq "http://localhost:1234/projects/123-foo" + expect(routes.project("123-foo")).to.eq "http://localhost:1234/projects/123-foo" + + it "projectRuns", -> + expect(routes.projectRuns("123-foo")).to.eq "http://localhost:1234/projects/123-foo/runs" it "projectToken", -> - expect(Routes.projectToken("123-foo")).to.eq "http://localhost:1234/projects/123-foo/token" + expect(routes.projectToken("123-foo")).to.eq "http://localhost:1234/projects/123-foo/token" it "exceptions", -> - expect(Routes.exceptions()).to.eq "http://localhost:1234/exceptions" + expect(routes.exceptions()).to.eq "http://localhost:1234/exceptions" diff --git a/packages/server/test/unit/saved_state_spec.coffee b/packages/server/test/unit/saved_state_spec.coffee index c42c8754b31e..7d7b5b55c5cc 100644 --- a/packages/server/test/unit/saved_state_spec.coffee +++ b/packages/server/test/unit/saved_state_spec.coffee @@ -1,25 +1,23 @@ require("../spec_helper") -fs = require("fs-extra") path = require("path") Promise = require("bluebird") +savedState = require("#{root}lib/saved_state") +fs = require("#{root}lib/util/fs") FileUtil = require("#{root}lib/util/file") appData = require("#{root}lib/util/app_data") -savedState = require("#{root}lib/saved_state") savedStateUtil = require("#{root}lib/util/saved_state") -fs = Promise.promisifyAll(fs) - describe "lib/util/saved_state", -> describe "project name hash", -> - projectPath = "/foo/bar" + projectRoot = "/foo/bar" it "starts with folder name", -> - hash = savedStateUtil.toHashName projectPath + hash = savedStateUtil.toHashName projectRoot expect(hash).to.match(/^bar-/) it "computed for given path", -> - hash = savedStateUtil.toHashName projectPath + hash = savedStateUtil.toHashName projectRoot expected = "bar-1df481b1ec67d4d8bec721f521d4937d" expect(hash).to.equal(expected) @@ -52,12 +50,12 @@ describe "lib/saved_state", -> b = savedState("/foo/baz") expect(a).to.not.equal(b) - it "sets path to project name + hash if projectPath", -> + it "sets path to project name + hash if projectRoot", -> savedState("/foo/the-project-name") .then (state) -> expect(state.path).to.include("the-project-name") - it "sets path __global__ if no projectPath", -> + it "sets path __global__ if no projectRoot", -> savedState() .then (state) -> expected = path.join(appData.path(), "projects", "__global__", "state.json") diff --git a/packages/server/test/unit/scaffold_spec.coffee b/packages/server/test/unit/scaffold_spec.coffee index 7f298272060c..5a0cb33b783d 100644 --- a/packages/server/test/unit/scaffold_spec.coffee +++ b/packages/server/test/unit/scaffold_spec.coffee @@ -1,17 +1,16 @@ require("../spec_helper") path = require("path") -glob = require("glob") Promise = require("bluebird") cypressEx = require("@packages/example") snapshot = require("snap-shot-it") config = require("#{root}lib/config") Project = require("#{root}lib/project") scaffold = require("#{root}lib/scaffold") +fs = require("#{root}lib/util/fs") +glob = require("#{root}lib/util/glob") Fixtures = require("#{root}/test/support/helpers/fixtures") -glob = Promise.promisify(glob) - describe "lib/scaffold", -> beforeEach -> Fixtures.scaffold() @@ -20,8 +19,8 @@ describe "lib/scaffold", -> Fixtures.remove() context ".integrationExampleName", -> - it "returns example_spec.js", -> - expect(scaffold.integrationExampleName()).to.eq("example_spec.js") + it "returns examples", -> + expect(scaffold.integrationExampleName()).to.eq("examples") context.skip ".isNewProject", -> beforeEach -> @@ -93,14 +92,17 @@ describe "lib/scaffold", -> config.get(pristinePath).then (@cfg) => {@integrationFolder} = @cfg - it "creates both integrationFolder and example_spec.js when integrationFolder does not exist", -> + it "creates both integrationFolder and example specs when integrationFolder does not exist", -> scaffold.integration(@integrationFolder, @cfg) .then => Promise.all([ - fs.statAsync(@integrationFolder + "/example_spec.js").get("size") - fs.statAsync(cypressEx.getPathToExample()).get("size") - ]).spread (size1, size2) -> + fs.statAsync(@integrationFolder + "/examples/actions.spec.js").get("size") + fs.statAsync(cypressEx.getPathToExamples()[0]).get("size") + fs.statAsync(@integrationFolder + "/examples/location.spec.js").get("size") + fs.statAsync(cypressEx.getPathToExamples()[8]).get("size") + ]).spread (size1, size2, size3, size4) -> expect(size1).to.eq(size2) + expect(size3).to.eq(size4) it "does not create any files if integrationFolder is not default", -> @cfg.resolved.integrationFolder.from = "config" @@ -111,7 +113,7 @@ describe "lib/scaffold", -> .then (files) -> expect(files.length).to.eq(0) - it "does not create example_spec.js if integrationFolder already exists", -> + it "does not create example specs if integrationFolder already exists", -> ## create the integrationFolder ourselves manually fs.ensureDirAsync(@integrationFolder) .then => diff --git a/packages/server/test/unit/screenshots_spec.coffee b/packages/server/test/unit/screenshots_spec.coffee index 22d9f0f1b69e..2ec8b99acf47 100644 --- a/packages/server/test/unit/screenshots_spec.coffee +++ b/packages/server/test/unit/screenshots_spec.coffee @@ -1,36 +1,296 @@ require("../spec_helper") +_ = require("lodash") path = require("path") +Jimp = require("jimp") +Buffer = require("buffer").Buffer +dataUriToBuffer = require("data-uri-to-buffer") +sizeOf = require("image-size") Fixtures = require("../support/helpers/fixtures") config = require("#{root}lib/config") -settings = require("#{root}lib/util/settings") screenshots = require("#{root}lib/screenshots") +fs = require("#{root}lib/util/fs") +settings = require("#{root}lib/util/settings") +screenshotAutomation = require("#{root}lib/automation/screenshot") image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALlJREFUeNpi1F3xYAIDA4MBA35wgQWqyB5dRoaVmeHJ779wPhOM0aQtyBAoyglmOwmwM6z1lWY44CMDFgcBFmRTGp3EGGJe/WIQ5mZm4GRlBGJmhlm3PqGaeODpNzCtKsbGIARUCALvvv6FWw9XeOvrH4bbQNOQwfabnzHdGK3AwyAjyAqX2HPzC0Pn7Y9wPtyNIMGlD74wmAqwMZz+8AvFxzATVZAFQIqwABWQiWtgAY5uCnKAAwQYAPr8OZysiz4PAAAAAElFTkSuQmCC" +iso8601Regex = /^\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}\.?\d*Z?$/ describe "lib/screenshots", -> beforeEach -> Fixtures.scaffold() - @todosPath = Fixtures.projectPath("todos") - config.get(@todosPath) - .then (@cfg) => + @appData = { + capture: "viewport" + clip: { x: 0, y: 0, width: 10, height: 10 } + viewport: { width: 40, height: 40 } + } + + @buffer = {} + + @jimpImage = { + bitmap: { + width: 40 + height: 40 + } + crop: sinon.stub() + getBuffer: sinon.stub().resolves(@buffer) + getMIME: -> "image/png" + hash: sinon.stub().returns("image hash") + clone: => @jimpImage + } + + Jimp.prototype.composite = sinon.stub() + Jimp.prototype.getBuffer = sinon.stub().resolves(@buffer) + + config.get(@todosPath).then (@config) => afterEach -> Fixtures.remove() + context ".capture", -> + beforeEach -> + @getPixelColor = sinon.stub() + @getPixelColor.withArgs(0, 0).returns("grey") + @getPixelColor.withArgs(1, 0).returns("white") + @getPixelColor.withArgs(0, 1).returns("white") + @getPixelColor.withArgs(40, 0).returns("white") + @getPixelColor.withArgs(0, 40).returns("white") + @getPixelColor.withArgs(40, 40).returns("black") + @jimpImage.getPixelColor = @getPixelColor + + sinon.stub(Jimp, "read").resolves(@jimpImage) + intToRGBA = sinon.stub(Jimp, "intToRGBA") + intToRGBA.withArgs("black").returns({ r: 0, g: 0, b: 0 }) + intToRGBA.withArgs("grey").returns({ r: 127, g: 127, b: 127 }) + intToRGBA.withArgs("white").returns({ r: 255, g: 255, b: 255 }) + + @automate = sinon.stub().resolves(image) + + @passPixelTest = => + @getPixelColor.withArgs(0, 0).returns("white") + + it "captures screenshot with automation", -> + data = { viewport: {} } + screenshots.capture(data, @automate).then => + expect(@automate).to.be.calledOnce + expect(@automate).to.be.calledWith(data) + + it "retries until helper pixels are no longer present for viewport capture", -> + @getPixelColor.withArgs(0, 0).onCall(1).returns("white") + screenshots.capture(@appData, @automate).then => + expect(@automate).to.be.calledTwice + + it "retries until helper pixels are present for runner capture", -> + @passPixelTest() + @getPixelColor.withArgs(0, 0).onCall(1).returns("black") + screenshots.capture({ viewport: {} }, @automate).then => + expect(@automate).to.be.calledTwice + + it "gives up after 10 tries", -> + screenshots.capture(@appData, @automate).then => + expect(@automate.callCount).to.equal(10) + + it "adjusts cropping based on pixel ratio", -> + @appData.viewport = { width: 20, height: 20 } + @appData.clip = { x: 5, y: 5, width: 10, height: 10 } + @passPixelTest() + screenshots.capture(@appData, @automate).then => + expect(@jimpImage.crop).to.be.calledWith(10, 10, 20, 20) + + it "resolves details w/ image", -> + @passPixelTest() + + screenshots.capture(@appData, @automate).then (details) => + expect(details.image).to.equal(@jimpImage) + expect(details.multipart).to.be.false + expect(details.pixelRatio).to.equal(1) + expect(details.takenAt).to.match(iso8601Regex) + + describe "simple capture", -> + beforeEach -> + @appData.simple = true + + it "skips pixel checking / reading into Jimp image", -> + screenshots.capture(@appData, @automate).then -> + expect(Jimp.read).not.to.be.called + + it "resolves details w/ buffer", -> + screenshots.capture(@appData, @automate).then (details) -> + expect(details.takenAt).to.match(iso8601Regex) + expect(details.multipart).to.be.false + expect(details.buffer).to.be.instanceOf(Buffer) + + describe "userClip", -> + it "crops final image if userClip specified", -> + @appData.userClip = { width: 5, height: 5, x: 2, y: 2 } + @passPixelTest() + screenshots.capture(@appData, @automate).then => + expect(@jimpImage.crop).to.be.calledWith(2, 2, 5, 5) + + it "does not crop intermediary multi-part images", -> + @appData.userClip = { width: 5, height: 5, x: 2, y: 2 } + @appData.current = 1 + @appData.total = 3 + @passPixelTest() + screenshots.capture(@appData, @automate).then => + expect(@jimpImage.crop).not.to.be.called + + it "adjusts cropping based on pixel ratio", -> + @appData.viewport = { width: 20, height: 20 } + @appData.userClip = { x: 5, y: 5, width: 10, height: 10 } + @passPixelTest() + screenshots.capture(@appData, @automate).then => + expect(@jimpImage.crop).to.be.calledWith(10, 10, 20, 20) + + describe "multi-part capture (fullPage or element)", -> + beforeEach -> + screenshots.clearMultipartState() + + @appData.current = 1 + @appData.total = 3 + + @getPixelColor.withArgs(0, 0).onCall(1).returns("white") + + it "retries until helper pixels are no longer present on first capture", -> + screenshots.capture(@appData, @automate).then => + expect(@automate).to.be.calledTwice + + it "retries until images aren't the same on subsequent captures", -> + @jimpImage2 = _.extend({}, @jimpImage, { + foo: true + hash: -> "image 2 hash" + }) + Jimp.read.onCall(3).resolves(@jimpImage2) + + screenshots.capture(@appData, @automate) + .then => + @appData.current = 2 + screenshots.capture(@appData, @automate) + .then => + expect(@automate.callCount).to.equal(4) + + it "resolves no image on non-last captures", -> + screenshots.capture(@appData, @automate).then (image) -> + expect(image).to.be.null + + it "resolves details w/ image on last capture", -> + screenshots.capture(@appData, @automate) + .then => + @appData.current = 3 + screenshots.capture(@appData, @automate) + .then ({ image }) => + expect(image).to.be.an.instanceOf(Jimp) + + it "composites images into one image", -> + screenshots.capture(@appData, @automate) + .then => + @appData.current = 2 + screenshots.capture(@appData, @automate) + .then => + @appData.current = 3 + screenshots.capture(@appData, @automate) + .then => + composite = Jimp.prototype.composite + expect(composite).to.be.calledThrice + expect(composite.getCall(0).args[0]).to.equal(@jimpImage) + expect(composite.getCall(0).args[1]).to.equal(0) + expect(composite.getCall(0).args[2]).to.equal(0) + expect(composite.getCall(1).args[0]).to.equal(@jimpImage) + expect(composite.getCall(1).args[2]).to.equal(40) + expect(composite.getCall(2).args[0]).to.equal(@jimpImage) + expect(composite.getCall(2).args[2]).to.equal(80) + + it "clears previous full page state once complete", -> + @appData.total = 2 + screenshots.capture(@appData, @automate) + .then => + @appData.current = 2 + screenshots.capture(@appData, @automate) + .then => + @appData.current = 1 + screenshots.capture(@appData, @automate) + .then => + @appData.current = 2 + screenshots.capture(@appData, @automate) + .then -> + expect(Jimp.prototype.composite.callCount).to.equal(4) + + it "skips full page process if only one capture needed", -> + @appData.total = 1 + screenshots.capture(@appData, @automate) + .then -> + expect(Jimp.prototype.composite).not.to.be.called + + context ".crop", -> + beforeEach -> + @dimensions = (overrides) -> + _.extend({ x: 0, y: 0, width: 10, height: 10 }, overrides) + + it "crops to dimension size if less than the image size", -> + screenshots.crop(@jimpImage, @dimensions()) + expect(@jimpImage.crop).to.be.calledWith(0, 0, 10, 10) + + it "crops to dimension size if less than the image size", -> + screenshots.crop(@jimpImage, @dimensions()) + expect(@jimpImage.crop).to.be.calledWith(0, 0, 10, 10) + + it "crops to one less than width if dimensions x is more than the image width", -> + screenshots.crop(@jimpImage, @dimensions({ x: 50 })) + expect(@jimpImage.crop).to.be.calledWith(39, 0, 1, 10) + + it "crops to one less than height if dimensions y is more than the image height", -> + screenshots.crop(@jimpImage, @dimensions({ y: 50 })) + expect(@jimpImage.crop).to.be.calledWith(0, 39, 10, 1) + + it "crops only width if dimensions height is more than the image height", -> + screenshots.crop(@jimpImage, @dimensions({ height: 50 })) + expect(@jimpImage.crop).to.be.calledWith(0, 0, 10, 40) + + it "crops only height if dimensions width is more than the image width", -> + screenshots.crop(@jimpImage, @dimensions({ width: 50 })) + expect(@jimpImage.crop).to.be.calledWith(0, 0, 40, 10) + context ".save", -> - it "outputs file and returns size and path", -> - screenshots.save({name: "foo/tweet"}, image, @cfg.screenshotsFolder) - .then (obj) => - expectedPath = path.normalize(@cfg.screenshotsFolder + "/footweet.png") - actualPath = path.normalize(obj.path) + it "outputs file and returns details", -> + details = { + image: @jimpImage + multipart: false + pixelRatio: 2 + takenAt: "taken:at:date" + } + + screenshots.save({name: "foo/tweet"}, details, @config.screenshotsFolder) + .then (result) => + expectedPath = path.normalize(@config.screenshotsFolder + "/footweet.png") + actualPath = path.normalize(result.path) + + expect(actualPath).to.eq(expectedPath) + expect(result.size).to.eq("15 B") + expect(result.dimensions).to.eql({ width: 40, height: 40 }) + expect(result.multipart).to.be.false + expect(result.pixelRatio).to.be.eq(2) + expect(result.takenAt).to.eq("taken:at:date") + + fs.statAsync(expectedPath) + + it "can handle saving buffer", -> + details = { + multipart: false + pixelRatio: 1 + buffer: dataUriToBuffer(image) + } + dimensions = sizeOf(details.buffer) + screenshots.save({name: "bar/tweet"}, details, @config.screenshotsFolder) + .then (result) => + expectedPath = path.normalize(@config.screenshotsFolder + "/bartweet.png") + actualPath = path.normalize(result.path) - expect(obj.size).to.eq("279 B") + expect(result.multipart).to.be.false + expect(result.pixelRatio).to.equal(1) expect(actualPath).to.eq(expectedPath) - expect(obj.width).to.eq(10) - expect(obj.height).to.eq(10) + expect(result.dimensions).to.eql(dimensions) fs.statAsync(expectedPath) @@ -39,6 +299,30 @@ describe "lib/screenshots", -> screenshots.copy("/does/not/exist", "/foo/bar/baz") it "copies src to des with {overwrite: true}", -> - @sandbox.stub(fs, "copyAsync").withArgs("foo", "bar", {overwrite: true}).resolves() + sinon.stub(fs, "copyAsync").withArgs("foo", "bar", {overwrite: true}).resolves() screenshots.copy("foo", "bar") + +describe "lib/automation/screenshot", -> + beforeEach -> + @image = {} + sinon.stub(screenshots, "capture").resolves(@image) + sinon.stub(screenshots, "save") + + @screenshot = screenshotAutomation("cypress/screenshots") + + it "captures screenshot", -> + data = {} + automation = -> + @screenshot.capture(data, automation).then -> + expect(screenshots.capture).to.be.calledWith(data, automation) + + it "saves screenshot if there's a buffer", -> + data = {} + @screenshot.capture(data, @automate).then => + expect(screenshots.save).to.be.calledWith(data, @image, "cypress/screenshots") + + it "does not save screenshot if there's no buffer", -> + screenshots.capture.resolves(null) + @screenshot.capture({}, @automate).then => + expect(screenshots.save).not.to.be.called diff --git a/packages/server/test/unit/security_spec.coffee b/packages/server/test/unit/security_spec.coffee index 26f367c644fa..1395232dd6f4 100644 --- a/packages/server/test/unit/security_spec.coffee +++ b/packages/server/test/unit/security_spec.coffee @@ -3,6 +3,7 @@ require("../spec_helper") _ = require("lodash") rp = require("request-promise") concat = require("concat-stream") +fs = require("#{root}lib/util/fs") security = require("#{root}lib/util/security") Fixtures = require("#{root}test/support/helpers/fixtures") @@ -119,6 +120,27 @@ describe "lib/util/security", -> it "replaces obstructive code", -> expect(security.strip(original)).to.eq(expected) + it "replaces jira window getter", -> + jira = """ + for (; !function (n) { + return n === n.parent + }(n) + """ + + jira2 = """ + function(n){for(;!function(l){return l===l.parent}(l)&&function(l){try{if(void 0==l.location.href)return!1}catch(l){return!1}return!0}(l.parent);)l=l.parent;return l} + """ + + expect(security.strip(jira)).to.eq(""" + for (; !function (n) { + return n === n.parent || n.parent.__Cypress__ + }(n) + """) + + expect(security.strip(jira2)).to.eq(""" + function(n){for(;!function(l){return l===l.parent || l.parent.__Cypress__}(l)&&function(l){try{if(void 0==l.location.href)return!1}catch(l){return!1}return!0}(l.parent);)l=l.parent;return l} + """) + describe "libs", -> ## go out and download all of these libs and ensure ## that we can run them through the security strip diff --git a/packages/server/test/unit/server_spec.coffee b/packages/server/test/unit/server_spec.coffee index ee6dc9c3a5bb..b1fb551ad0f6 100644 --- a/packages/server/test/unit/server_spec.coffee +++ b/packages/server/test/unit/server_spec.coffee @@ -22,7 +22,7 @@ describe "lib/server", -> close: -> port: -> 1111 } - @sandbox.stub(fileServer, "create").returns(@fileServer) + sinon.stub(fileServer, "create").returns(@fileServer) config.set({projectRoot: "/foo/bar/"}) .then (cfg) => @@ -36,7 +36,7 @@ describe "lib/server", -> context "#createExpressApp", -> beforeEach -> - @use = @sandbox.spy(express.application, "use") + @use = sinon.spy(express.application, "use") it "instantiates express instance without morgan", -> app = @server.createExpressApp(false) @@ -49,10 +49,10 @@ describe "lib/server", -> context "#open", -> beforeEach -> - @sandbox.stub(@server, "createServer").resolves() + sinon.stub(@server, "createServer").resolves() it "calls #createExpressApp with morgan", -> - @sandbox.spy(@server, "createExpressApp") + sinon.spy(@server, "createExpressApp") _.extend @config, {port: 54321, morgan: false} @@ -65,8 +65,8 @@ describe "lib/server", -> obj = {} - @sandbox.stub(@server, "createRoutes") - @sandbox.stub(@server, "createExpressApp").returns(obj) + sinon.stub(@server, "createRoutes") + sinon.stub(@server, "createExpressApp").returns(obj) @server.open(@config) .then => @@ -75,8 +75,8 @@ describe "lib/server", -> it "calls #createRoutes with app + config", -> obj = {} - @sandbox.stub(@server, "createRoutes") - @sandbox.stub(@server, "createExpressApp").returns(obj) + sinon.stub(@server, "createRoutes") + sinon.stub(@server, "createExpressApp").returns(obj) @server.open(@config) .then => @@ -85,15 +85,15 @@ describe "lib/server", -> it "calls #createServer with port + fileServerFolder + socketIoRoute + app", -> obj = {} - @sandbox.stub(@server, "createRoutes") - @sandbox.stub(@server, "createExpressApp").returns(obj) + sinon.stub(@server, "createRoutes") + sinon.stub(@server, "createExpressApp").returns(obj) @server.open(@config) .then => expect(@server.createServer).to.be.calledWith(obj, @config) it "calls logger.setSettings with config", -> - @sandbox.spy(logger, "setSettings") + sinon.spy(logger, "setSettings") @server.open(@config) .then (ret) => @@ -115,7 +115,7 @@ describe "lib/server", -> expect(port).to.eq(@port) it "resolves with warning if cannot connect to baseUrl", -> - @sandbox.stub(connect, "ensureUrl").rejects() + sinon.stub(connect, "ensureUrl").rejects() @server.createServer(@app, {port: @port, baseUrl: "http://localhost:#{@port}"}) .spread (port, warning) => expect(warning.type).to.eq("CANNOT_CONNECT_BASE_URL_WARNING") @@ -134,7 +134,7 @@ describe "lib/server", -> context "#end", -> it "calls this._socket.end", -> - socket = @sandbox.stub({ + socket = sinon.stub({ end: -> close: -> }) @@ -149,7 +149,7 @@ describe "lib/server", -> context "#startWebsockets", -> beforeEach -> - @startListening = @sandbox.stub(Socket.prototype, "startListening") + @startListening = sinon.stub(Socket.prototype, "startListening") it "sets _socket and calls _socket#startListening", -> @server.open(@config) @@ -160,7 +160,7 @@ describe "lib/server", -> context "#reset", -> beforeEach -> - @sandbox.stub(buffers, "reset") + sinon.stub(buffers, "reset") it "resets the buffers", -> @server.reset() @@ -199,7 +199,7 @@ describe "lib/server", -> expect(logger.getSettings()).to.be.undefined it "calls close on this._socket", -> - @server._socket = {close: @sandbox.spy()} + @server._socket = {close: sinon.spy()} @server.close() .then => @@ -207,8 +207,11 @@ describe "lib/server", -> context "#proxyWebsockets", -> beforeEach -> - @proxy = @sandbox.stub({ws: ->}) - @socket = @sandbox.stub({end: ->}) + @proxy = sinon.stub({ + ws: -> + on: -> + }) + @socket = sinon.stub({end: ->}) @head = {} it "is noop if req.url startsWith socketIoRoute", -> diff --git a/packages/server/test/unit/settings_spec.coffee b/packages/server/test/unit/settings_spec.coffee index e9c6f8a5de01..43ceaf0b6b7d 100644 --- a/packages/server/test/unit/settings_spec.coffee +++ b/packages/server/test/unit/settings_spec.coffee @@ -2,6 +2,7 @@ require("../spec_helper") path = require("path") R = require("ramda") +fs = require("#{root}lib/util/fs") settings = require("#{root}lib/util/settings") projectRoot = process.cwd() @@ -60,18 +61,18 @@ describe "lib/settings", -> context ".id", -> beforeEach -> - @projectPath = path.join(projectRoot, "_test-output/path/to/project/") - fs.ensureDirAsync(@projectPath) + @projectRoot = path.join(projectRoot, "_test-output/path/to/project/") + fs.ensureDirAsync(@projectRoot) afterEach -> - fs.removeAsync("#{@projectPath}cypress.json") + fs.removeAsync("#{@projectRoot}cypress.json") it "returns project id for project", -> - fs.writeJsonAsync("#{@projectPath}cypress.json", { + fs.writeJsonAsync("#{@projectRoot}cypress.json", { projectId: "id-123" }) .then => - settings.id(@projectPath) + settings.id(@projectRoot) .then (id) -> expect(id).to.equal("id-123") diff --git a/packages/server/test/unit/socket_spec.coffee b/packages/server/test/unit/socket_spec.coffee index 7e6738803ff9..a8258293e728 100644 --- a/packages/server/test/unit/socket_spec.coffee +++ b/packages/server/test/unit/socket_spec.coffee @@ -8,16 +8,17 @@ Promise = require("bluebird") socketIo = require("@packages/socket") extension = require("@packages/extension") httpsAgent = require("https-proxy-agent") -open = require("#{root}lib/util/open") errors = require("#{root}lib/errors") config = require("#{root}lib/config") Socket = require("#{root}lib/socket") Server = require("#{root}lib/server") Automation = require("#{root}lib/automation") -Fixtures = require("#{root}/test/support/helpers/fixtures") exec = require("#{root}lib/exec") savedState = require("#{root}lib/saved_state") preprocessor = require("#{root}lib/plugins/preprocessor") +fs = require("#{root}lib/util/fs") +open = require("#{root}lib/util/open") +Fixtures = require("#{root}/test/support/helpers/fixtures") describe "lib/socket", -> beforeEach -> @@ -40,7 +41,7 @@ describe "lib/socket", -> @server.open(@cfg) .then => @options = { - onSavedStateChanged: @sandbox.spy() + onSavedStateChanged: sinon.spy() } @automation = Automation.create(@cfg.namespace, @cfg.socketIoCookie, @cfg.screenshotsFolder) @@ -105,7 +106,7 @@ describe "lib/socket", -> delete global.chrome it "does not return cypress namespace or socket io cookies", (done) -> - @sandbox.stub(chrome.cookies, "getAll") + sinon.stub(chrome.cookies, "getAll") .withArgs({domain: "localhost"}) .yieldsAsync([ {name: "foo", value: "f", path: "/", domain: "localhost", secure: true, httpOnly: true, expirationDate: 123, a: "a", b: "c"} @@ -125,13 +126,13 @@ describe "lib/socket", -> done() it "does not clear any namespaced cookies", (done) -> - @sandbox.stub(chrome.cookies, "getAll") + sinon.stub(chrome.cookies, "getAll") .withArgs({name: "session"}) .yieldsAsync([ {name: "session", value: "key", path: "/", domain: "google.com", secure: true, httpOnly: true, expirationDate: 123, a: "a", b: "c"} ]) - @sandbox.stub(chrome.cookies, "remove") + sinon.stub(chrome.cookies, "remove") .withArgs({name: "session", url: "https://google.com/"}) .yieldsAsync( {name: "session", url: "https://google.com/", storeId: "123"} @@ -169,11 +170,11 @@ describe "lib/socket", -> it "returns true when tab matches magic string", (done) -> code = "var s; (s = document.getElementById('__cypress-string')) && s.textContent" - @sandbox.stub(chrome.tabs, "query") + sinon.stub(chrome.tabs, "query") .withArgs({windowType: "normal"}) .yieldsAsync([{id: 1, url: "http://localhost"}]) - @sandbox.stub(chrome.tabs, "executeScript") + sinon.stub(chrome.tabs, "executeScript") .withArgs(1, {code: code}) .yieldsAsync(["string"]) @@ -182,10 +183,10 @@ describe "lib/socket", -> done() it "returns true after retrying", (done) -> - @sandbox.stub(extension.app, "query").resolves(true) + sinon.stub(extension.app, "query").resolves(true) ## just force isSocketConnected to return false until the 4th retry - iSC = @sandbox.stub(@socket, "isSocketConnected") + iSC = sinon.stub(@socket, "isSocketConnected") iSC .onCall(0).returns(false) @@ -205,11 +206,11 @@ describe "lib/socket", -> it "returns false when times out", (done) -> code = "var s; (s = document.getElementById('__cypress-string')) && s.textContent" - @sandbox.stub(chrome.tabs, "query") + sinon.stub(chrome.tabs, "query") .withArgs({url: "CHANGE_ME_HOST/*", windowType: "normal"}) .yieldsAsync([{id: 1}]) - @sandbox.stub(chrome.tabs, "executeScript") + sinon.stub(chrome.tabs, "executeScript") .withArgs(1, {code: code}) .yieldsAsync(["foobarbaz"]) @@ -220,7 +221,7 @@ describe "lib/socket", -> it "retries multiple times and stops after timing out", (done) -> ## just force isSocketConnected to return false until the 4th retry - iSC = @sandbox.stub(@socket, "isSocketConnected") + iSC = sinon.stub(@socket, "isSocketConnected") ## reduce the timeout so we dont have to wait so long @client.emit "is:automation:client:connected", {element: "__cypress-string", string: "string", timeout: 100}, (resp) -> @@ -246,7 +247,7 @@ describe "lib/socket", -> describe "options.onAutomationRequest", -> beforeEach -> - @ar = @sandbox.stub(@automation, "request") + @ar = sinon.stub(@automation, "request") it "calls onAutomationRequest with message and data", (done) -> @ar.withArgs("focus", {foo: "bar"}).resolves([]) @@ -284,7 +285,7 @@ describe "lib/socket", -> it "emits 'automation:push:message'", (done) -> data = {cause: "explicit", cookie: {name: "foo", value: "bar"}, removed: true} - emit = @sandbox.stub(@socket.io, "emit") + emit = sinon.stub(@socket.io, "emit") @client.emit "automation:push:request", "change:cookie", data, -> expect(emit).to.be.calledWith("automation:push:message", "change:cookie", { @@ -296,7 +297,7 @@ describe "lib/socket", -> context "on(open:finder)", -> beforeEach -> - @sandbox.stub(open, "opn").resolves() + sinon.stub(open, "opn").resolves() it "calls opn with path", (done) -> @client.emit "open:finder", @cfg.parentTestsFolder, => @@ -305,7 +306,7 @@ describe "lib/socket", -> context "on(watch:test:file)", -> it "calls socket#watchTestFileByPath with config, filePath", (done) -> - @sandbox.stub(@socket, "watchTestFileByPath") + sinon.stub(@socket, "watchTestFileByPath") @client.emit "watch:test:file", "path/to/file", => expect(@socket.watchTestFileByPath).to.be.calledWith(@cfg, "path/to/file") @@ -339,7 +340,7 @@ describe "lib/socket", -> context "on(http:request)", -> it "calls socket#onRequest", (done) -> - @sandbox.stub(@options, "onRequest").resolves({foo: "bar"}) + sinon.stub(@options, "onRequest").resolves({foo: "bar"}) @client.emit "backend:request", "http:request", "foo", (resp) -> expect(resp.response).to.deep.eq({foo: "bar"}) @@ -349,7 +350,7 @@ describe "lib/socket", -> it "catches errors and clones them", (done) -> err = new Error("foo bar baz") - @sandbox.stub(@options, "onRequest").rejects(err) + sinon.stub(@options, "onRequest").rejects(err) @client.emit "backend:request", "http:request", "foo", (resp) -> expect(resp.error).to.deep.eq(errors.clone(err)) @@ -358,21 +359,21 @@ describe "lib/socket", -> context "on(exec)", -> it "calls exec#run with project root and options", (done) -> - run = @sandbox.stub(exec, "run").returns(Promise.resolve("Desktop Music Pictures")) + run = sinon.stub(exec, "run").returns(Promise.resolve("Desktop Music Pictures")) @client.emit "backend:request", "exec", { cmd: "ls" }, (resp) => expect(run).to.be.calledWith(@cfg.projectRoot, { cmd: "ls" }) expect(resp.response).to.eq("Desktop Music Pictures") done() - it "errors when execution fails, passing through timedout", (done) -> + it "errors when execution fails, passing through timedOut", (done) -> error = new Error("command not found: lsd") - error.timedout = true - @sandbox.stub(exec, "run").rejects(error) + error.timedOut = true + sinon.stub(exec, "run").rejects(error) @client.emit "backend:request", "exec", { cmd: "lsd" }, (resp) => expect(resp.error.message).to.equal("command not found: lsd") - expect(resp.error.timedout).to.be.true + expect(resp.error.timedOut).to.be.true done() context "on(save:app:state)", -> @@ -383,20 +384,20 @@ describe "lib/socket", -> context "unit", -> beforeEach -> - @mockClient = @sandbox.stub({ + @mockClient = sinon.stub({ on: -> emit: -> }) @io = { - of: @sandbox.stub().returns({on: ->}) - on: @sandbox.stub().withArgs("connection").yields(@mockClient) - emit: @sandbox.stub() - close: @sandbox.stub() + of: sinon.stub().returns({on: ->}) + on: sinon.stub().withArgs("connection").yields(@mockClient) + emit: sinon.stub() + close: sinon.stub() } - @sandbox.stub(Socket.prototype, "createIo").returns(@io) - @sandbox.stub(preprocessor.emitter, "on") + sinon.stub(Socket.prototype, "createIo").returns(@io) + sinon.stub(preprocessor.emitter, "on") @server.open(@cfg) .then => @@ -431,7 +432,7 @@ describe "lib/socket", -> @socket.testsDir = Fixtures.project "todos/tests" @filePath = @socket.testsDir + "/test1.js" - @sandbox.stub(preprocessor, "getFile").resolves() + sinon.stub(preprocessor, "getFile").resolves() it "returns undefined if trying to watch special path __all", -> result = @socket.watchTestFileByPath(@cfg, "integration/__all") @@ -443,7 +444,7 @@ describe "lib/socket", -> expect(result).to.be.undefined it "closes existing watched test file", -> - @sandbox.stub(preprocessor, "removeFile") + sinon.stub(preprocessor, "removeFile") @socket.testFilePath = "tests/test1.js" @socket.watchTestFileByPath(@cfg, "test2.js").then => expect(preprocessor.removeFile).to.be.calledWithMatch("test1.js", @cfg) @@ -461,7 +462,7 @@ describe "lib/socket", -> expect(preprocessor.getFile).to.be.calledWith("tests/test2.coffee", @cfg) it "triggers watched:file:changed event when preprocessor 'file:updated' is received", (done) -> - @sandbox.stub(fs, "statAsync").resolves() + sinon.stub(fs, "statAsync").resolves() @cfg.watchForFileChanges = true @socket.watchTestFileByPath(@cfg, "integration/test2.coffee") preprocessor.emitter.on.withArgs("file:updated").yield("integration/test2.coffee") @@ -483,7 +484,7 @@ describe "lib/socket", -> expect(@mockClient.on).to.be.calledWith("watch:test:file") it "passes filePath to #watchTestFileByPath", -> - watchTestFileByPath = @sandbox.stub(@socket, "watchTestFileByPath") + watchTestFileByPath = sinon.stub(@socket, "watchTestFileByPath") @mockClient.on.withArgs("watch:test:file").yields("foo/bar/baz") @@ -492,7 +493,7 @@ describe "lib/socket", -> describe "#onTestFileChange", -> beforeEach -> - @sandbox.spy(fs, "statAsync") + sinon.spy(fs, "statAsync") it "calls statAsync on .js file", -> @socket.onTestFileChange("foo/bar.js").catch(->).then => diff --git a/packages/server/test/unit/spec_spec.coffee b/packages/server/test/unit/spec_spec.coffee index af40269d0407..c136d1f3f085 100644 --- a/packages/server/test/unit/spec_spec.coffee +++ b/packages/server/test/unit/spec_spec.coffee @@ -13,17 +13,17 @@ describe "lib/controllers/spec", -> beforeEach -> @project = { - emit: @sandbox.spy() + emit: sinon.spy() } @res = { - set: @sandbox.spy() - type: @sandbox.spy() - send: @sandbox.spy() - sendFile: @sandbox.spy() + set: sinon.spy() + type: sinon.spy() + send: sinon.spy() + sendFile: sinon.spy() } - @sandbox.stub(preprocessor, "getFile").resolves(outputFilePath) + sinon.stub(preprocessor, "getFile").resolves(outputFilePath) @handle = (filePath, config = {}) => spec.handle(filePath, {}, @res, config, (->), @project) @@ -39,7 +39,7 @@ describe "lib/controllers/spec", -> @handle(specName).then => expect(@res.sendFile).to.be.calledWith(outputFilePath) - it "sends a client-side error in headed mode", -> + it "sends a client-side error in interactive mode", -> preprocessor.getFile.rejects(new Error("Reason request failed")) @handle(specName).then => @@ -47,8 +47,8 @@ describe "lib/controllers/spec", -> expect(@res.send.firstCall.args[0]).to.include("(function") expect(@res.send.firstCall.args[0]).to.include("Reason request failed") - it "logs the error and exits in headless mode", -> - @sandbox.stub(errors, "log") + it "logs the error and exits in run mode", -> + sinon.stub(errors, "log") preprocessor.getFile.rejects(new Error("Reason request failed")) @handle(specName, {isTextTerminal: true}).then => diff --git a/packages/server/test/unit/specs_spec.coffee b/packages/server/test/unit/specs_spec.coffee new file mode 100644 index 000000000000..c59dc85688c5 --- /dev/null +++ b/packages/server/test/unit/specs_spec.coffee @@ -0,0 +1,79 @@ +require("../spec_helper") + +R = require("ramda") +path = require("path") +files = require("#{root}lib/files") +config = require("#{root}lib/config") +specsUtil = require("#{root}lib/util/specs") +FixturesHelper = require("#{root}/test/support/helpers/fixtures") + +describe "lib/util/specs", -> + beforeEach -> + FixturesHelper.scaffold() + + @todosPath = FixturesHelper.projectPath("todos") + + config.get(@todosPath) + .then (cfg) => + @config = cfg + + afterEach -> + FixturesHelper.remove() + + context ".find", -> + checkFoundSpec = (foundSpec) -> + if not path.isAbsolute(foundSpec.absolute) + throw new Error("path to found spec should be absolute #{JSON.stringify(foundSpec)}") + + it "returns absolute filenames", -> + specsUtil + .find(@config) + .then (R.forEach(checkFoundSpec)) + + it "handles fixturesFolder being false", -> + @config.fixturesFolder = false + + fn = => specsUtil.find(@config) + + expect(fn).not.to.throw() + + it "by default, returns all files as long as they have a name and extension", -> + config.get(FixturesHelper.projectPath("various-file-types")) + .then (cfg) -> + specsUtil.find(cfg) + .then (files) -> + expect(files.length).to.equal(3) + expect(files[0].name).to.equal("coffee_spec.coffee") + expect(files[1].name).to.equal("js_spec.js") + expect(files[2].name).to.equal("ts_spec.ts") + + it "returns files matching config.testFiles", -> + config.get(FixturesHelper.projectPath("various-file-types")) + .then (cfg) -> + cfg.testFiles = "**/*.coffee" + specsUtil.find(cfg) + .then (files) -> + expect(files.length).to.equal(1) + expect(files[0].name).to.equal("coffee_spec.coffee") + + it "filters using specPattern", -> + config.get(FixturesHelper.projectPath("various-file-types")) + .then (cfg) -> + specsUtil.find(cfg, [ + path.join(cfg.projectRoot, "cypress", "integration", "js_spec.js") + ]) + .then (files) -> + expect(files.length).to.equal(1) + expect(files[0].name).to.equal("js_spec.js") + + it "filters using specPattern as array of glob patterns", -> + config.get(FixturesHelper.projectPath("various-file-types")) + .then (cfg) -> + specsUtil.find(cfg, [ + path.join(cfg.projectRoot, "cypress", "integration", "js_spec.js") + path.join(cfg.projectRoot, "cypress", "integration", "ts*") + ]) + .then (files) -> + expect(files.length).to.equal(2) + expect(files[0].name).to.equal("js_spec.js") + expect(files[1].name).to.equal("ts_spec.ts") diff --git a/packages/server/test/unit/task_spec.coffee b/packages/server/test/unit/task_spec.coffee new file mode 100644 index 000000000000..2c8e57897e31 --- /dev/null +++ b/packages/server/test/unit/task_spec.coffee @@ -0,0 +1,46 @@ +require("../spec_helper") + +_ = require("lodash") +Promise = require("bluebird") +plugins = require("#{root}lib/plugins") +task = require("#{root}lib/task") + +fail = (message) -> throw new Error(message) + +describe "lib/task", -> + beforeEach -> + @pluginsFile = "cypress/plugins" + sinon.stub(plugins, "execute").resolves("result") + sinon.stub(plugins, "has").returns(true) + + it "executes the 'task' plugin", -> + task.run(@pluginsFile, { task: "some:task", arg: "some:arg", timeout: 1000 }).then -> + expect(plugins.execute).to.be.calledWith("task", "some:task", "some:arg") + + it "resolves the result of the 'task' plugin", -> + task.run(@pluginsFile, { task: "some:task", arg: "some:arg", timeout: 1000 }).then (result) -> + expect(result).to.equal("result") + + it "throws if 'task' event is not registered", -> + plugins.has.returns(false) + + task.run(@pluginsFile, { timeout: 1000 }).catch (err) => + expect(err.message).to.equal("The 'task' event has not been registered in the plugins file. You must register it before using cy.task()\n\nFix this in your plugins file here:\n#{@pluginsFile}\n\nhttps://on.cypress.io/api/task") + + it "throws if 'task' event resolves __cypress_unhandled__", -> + plugins.execute.withArgs("task").resolves("__cypress_unhandled__") + plugins.execute.withArgs("_get:task:keys").resolves(["foo", "bar"]) + task.run(@pluginsFile, { task: "some:task", arg: "some:arg", timeout: 1000 }).catch (err) => + expect(err.message).to.equal("The task 'some:task' was not handled in the plugins file. The following tasks are registered: foo, bar\n\nFix this in your plugins file here:\n#{@pluginsFile}\n\nhttps://on.cypress.io/api/task") + + it "throws if 'task' event resolves undefined", -> + plugins.execute.withArgs("task").resolves(undefined) + plugins.execute.withArgs("_get:task:body").resolves("function () {}") + task.run(@pluginsFile, { task: "some:task", arg: "some:arg", timeout: 1000 }).catch (err) => + expect(err.message).to.equal("The task 'some:task' returned undefined. You must return a promise, a value, or null to indicate that the task was handled.\n\nThe task handler was:\n\nfunction () {}\n\nFix this in your plugins file here:\n#{@pluginsFile}\n\nhttps://on.cypress.io/api/task") + + it "throws if it times out", -> + plugins.execute.withArgs("task").resolves(Promise.delay(250)) + plugins.execute.withArgs("_get:task:body").resolves("function () {}") + task.run(@pluginsFile, { task: "some:task", arg: "some:arg", timeout: 10 }).catch (err) => + expect(err.message).to.equal("The task handler was:\n\nfunction () {}\n\nFix this in your plugins file here:\n#{@pluginsFile}\n\nhttps://on.cypress.io/api/task") diff --git a/packages/server/test/unit/terminal_spec.coffee b/packages/server/test/unit/terminal_spec.coffee new file mode 100644 index 000000000000..d7a3efc822ae --- /dev/null +++ b/packages/server/test/unit/terminal_spec.coffee @@ -0,0 +1,136 @@ +require("../spec_helper") + +_ = require("lodash") +snapshot = require("snap-shot-it") +stripAnsi = require("strip-ansi") +widestLine = require("widest-line") +terminal = require("#{root}lib/util/terminal") +terminalSize = require("#{root}lib/util/terminal-size") + +sanitizeSnapshot = (str) -> + snapshot(stripAnsi(str)) + +render = (tables...) -> + str = terminal.renderTables(tables...) + + console.log(str) + + str + +expectLength = (str, length) -> + lineLength = widestLine(str.split("\n")[0]) + + ## first line should always be 100 chars + expect(lineLength).to.eq(length) + +describe "lib/util/terminal", -> + context ".convertDecimalsToNumber", -> + it "divides colWidths by cols", -> + expect(terminal.convertDecimalsToNumber( + [25, 25, 5, 15, 15, 15], 200 + )).to.deep.eq([ + 50, 50, 10, 30, 30, 30 + ]) + + it "adds remainder to first index", -> + expect(terminal.convertDecimalsToNumber( + [50, 50], 15 + )).to.deep.eq([ + 8, 7 + ]) + + context ".getMaximumColumns", -> + it "uses max 100 when exceeds terminalSize", -> + sinon.stub(terminalSize, "get").returns({ columns: 1000 }) + expect(terminal.getMaximumColumns()).to.eq(100) + + it "uses terminalSize when less than 100", -> + sinon.stub(terminalSize, "get").returns({ columns: 99 }) + expect(terminal.getMaximumColumns()).to.eq(99) + + context ".table", -> + beforeEach -> + sinon.stub(terminalSize, "get").returns({ columns: 200 }) + + it "draws multiple specs summary table", -> + colAligns = ["left", "right", "right", "right", "right", "right", "right"] + colWidths = [40, 10, 10, 10, 10, 10, 10] + + table1 = terminal.table({ + colAligns + colWidths + type: "noBorder" + head: [" Spec", "", "Tests", "Passing", "Failing", "Pending", "Skipped"] + }) + + table2 = terminal.table({ + colAligns + colWidths + type: "border" + }) + + table3 = terminal.table({ + colAligns + colWidths + type: "noBorder" + head: ["2 of 3 passed (66%)", "5m 36s", 37, 29, 8, 102, 18] + style: { + "padding-right": 2 + } + }) + + table2.push( + ["foo.js", "49s", 7, 4, 3, 2, 1] + ["bar.js", "6s", 0, 0, 0, 0, 15] + ["fail/is/whale.js", "3m 28s", 30, 25, 5, 100, 3] + ) + + str = render(table1, table2, table3) + + expectLength(str, 100) + + sanitizeSnapshot(str) + + it "draws single spec summary table", -> + table = terminal.table({ + type: "outsideBorder" + }) + + table.push( + ["Tests:", 1] + ["Passing:", 2] + ["Failing:", 3] + ["Pending:", 4] + ["Skipped:", 5] + ["Duration:", 6] + ["Screenshots:", 7] + ["Video:", true] + ["Spec:", "foo/bar/baz.js"] + ) + + str = render(table) + + sanitizeSnapshot(str) + + it "draws a page divider", -> + table = terminal.table({ + colWidths: [80, 20] + colAligns: ["left", "right"] + type: "pageDivider" + style: { + "padding-left": 2 + "padding-right": 1 + } + }) + + table.push(["", ""]) + table.push([ + "Running: foo/bar/baz.js...", + "(100 of 200)" + ]) + + str = render(table) + + expectLength(str, 100) + + sanitizeSnapshot(str) diff --git a/packages/server/test/unit/timers_spec.coffee b/packages/server/test/unit/timers_spec.coffee index 3f2a479e0665..a3a93bf88db2 100644 --- a/packages/server/test/unit/timers_spec.coffee +++ b/packages/server/test/unit/timers_spec.coffee @@ -87,7 +87,7 @@ describe "timers/parent", -> done() , 100 - fn = @sandbox.spy(poller) + fn = sinon.spy(poller) t = setInterval(fn, 10) diff --git a/packages/server/test/unit/tty_spec.coffee b/packages/server/test/unit/tty_spec.coffee index b2f256c3a238..2c39a7a624fb 100644 --- a/packages/server/test/unit/tty_spec.coffee +++ b/packages/server/test/unit/tty_spec.coffee @@ -3,46 +3,57 @@ require("../spec_helper") tty = require("tty") ttyUtil = require("#{root}lib/util/tty") -isTTY = process.stderr.isTTY +ttys = [process.stdin.isTTY, process.stdout.isTTY, process.stderr.isTTY] describe "lib/util/tty", -> context ".override", -> beforeEach -> + process.env.FORCE_STDIN_TTY = '1' + process.env.FORCE_STDOUT_TTY = '1' process.env.FORCE_STDERR_TTY = '1' ## do this so can we see when its modified - process.stderr.isTTY = undefined + process.stdin.isTTY = 'foo' + process.stdout.isTTY = 'foo' + process.stderr.isTTY = 'foo' afterEach -> ## restore sanity - delete process.env.FORCE_STDERR_TTY - process.stderr.isTTY = isTTY + process.stdin.isTTY = ttys[0] + process.stdout.isTTY = ttys[1] + process.stderr.isTTY = ttys[2] - it "is noop when not process.env.FORCE_STDERR_TTY", -> + it "is noop when not forcing in env", -> + delete process.env.FORCE_STDIN_TTY + delete process.env.FORCE_STDOUT_TTY delete process.env.FORCE_STDERR_TTY - expect(ttyUtil.override()).to.be.undefined + ttyUtil.override() - expect(process.stderr.isTTY).to.be.undefined + expect(process.stdin.isTTY).to.eq('foo') + expect(process.stdout.isTTY).to.eq('foo') + expect(process.stderr.isTTY).to.eq('foo') it "forces process.stderr.isTTY to be true", -> ttyUtil.override() + expect(process.stdin.isTTY).to.be.true + expect(process.stdout.isTTY).to.be.true expect(process.stderr.isTTY).to.be.true - it "modies isatty calls for stderr", -> - fd0 = tty.isatty(0) - fd1 = tty.isatty(1) + it "modifies isatty calls", -> + delete process.env.FORCE_STDERR_TTY - isatty = @sandbox.spy(tty, 'isatty') + isatty = sinon.spy(tty, 'isatty') ttyUtil.override() - expect(tty.isatty(0)).to.eq(fd0) - expect(isatty.firstCall).to.be.calledWith(0) - - expect(tty.isatty(1)).to.eq(fd1) - expect(isatty.secondCall).to.be.calledWith(1) + ## should slurp up the first two calls + ## and only proxy through the 3rd call + ## for stderr + tty.isatty(0) + tty.isatty(1) + tty.isatty(2) - expect(tty.isatty(2)).to.be.true - expect(isatty).not.to.be.calledThrice + expect(isatty.callCount).to.eq(1) + expect(isatty.firstCall).to.be.calledWith(2) diff --git a/packages/server/test/unit/updater_spec.coffee b/packages/server/test/unit/updater_spec.coffee index 3ebbbdc06a45..7cdba90ad315 100644 --- a/packages/server/test/unit/updater_spec.coffee +++ b/packages/server/test/unit/updater_spec.coffee @@ -1,7 +1,5 @@ require("../spec_helper") -delete global.fs - nmi = require("node-machine-id") cwd = require("#{root}lib/cwd") request = require("request") @@ -43,7 +41,7 @@ describe "lib/updater", -> context "#checkNewVersion", -> beforeEach -> - @get = @sandbox.spy(request, "get") + @get = sinon.spy(request, "get") @updater = Updater({}) @@ -68,7 +66,7 @@ describe "lib/updater", -> done() it "sends x-machine-id as null on error", (done) -> - @sandbox.stub(nmi, "machineId").rejects(new Error()) + sinon.stub(nmi, "machineId").rejects(new Error()) @updater.getClient().checkNewVersion => expect(@get).to.be.calledWithMatch({ @@ -81,9 +79,9 @@ describe "lib/updater", -> context "#check", -> beforeEach -> - @updater = Updater({quit: @sandbox.spy()}) + @updater = Updater({quit: sinon.spy()}) @updater.getClient() - @sandbox.stub(@updater.client, "checkNewVersion") + sinon.stub(@updater.client, "checkNewVersion") it "calls checkNewVersion", -> @updater.check() @@ -92,7 +90,7 @@ describe "lib/updater", -> it "calls options.newVersionExists when there is a no version", -> @updater.client.checkNewVersion.yields(null, true, {}) - options = {onNewVersion: @sandbox.spy()} + options = {onNewVersion: sinon.spy()} @updater.check(options) expect(options.onNewVersion).to.be.calledWith({}) @@ -100,7 +98,7 @@ describe "lib/updater", -> it "calls options.newVersionExists when there is a no version", -> @updater.client.checkNewVersion.yields(null, false) - options = {onNoNewVersion: @sandbox.spy()} + options = {onNoNewVersion: sinon.spy()} @updater.check(options) expect(options.onNoNewVersion).to.be.called diff --git a/packages/server/test/unit/user_spec.coffee b/packages/server/test/unit/user_spec.coffee index 4f727c7cb480..cf43e96c3aed 100644 --- a/packages/server/test/unit/user_spec.coffee +++ b/packages/server/test/unit/user_spec.coffee @@ -8,7 +8,7 @@ errors = require("#{root}lib/errors") describe "lib/user", -> context ".get", -> it "calls cache.getUser", -> - @sandbox.stub(cache, "getUser").resolves({name: "brian"}) + sinon.stub(cache, "getUser").resolves({name: "brian"}) user.get().then (user) -> expect(user).to.deep.eq({name: "brian"}) @@ -16,8 +16,8 @@ describe "lib/user", -> context ".logIn", -> it "sets user to cache + returns user", -> obj = {name: "brian"} - @sandbox.stub(api, "createSignin").withArgs("abc-123").resolves(obj) - @sandbox.spy(cache, "setUser") + sinon.stub(api, "createSignin").withArgs("abc-123").resolves(obj) + sinon.spy(cache, "setUser") user.logIn("abc-123").then (ret) -> expect(ret).to.deep.eq(obj) @@ -25,46 +25,46 @@ describe "lib/user", -> context ".logOut", -> it "calls api.createSignout + removes the session from cache", -> - @sandbox.stub(api, "createSignout").withArgs("abc-123").resolves() - @sandbox.stub(cache, "getUser").resolves({name: "brian", authToken: "abc-123"}) - @sandbox.spy(cache, "removeUser") + sinon.stub(api, "createSignout").withArgs("abc-123").resolves() + sinon.stub(cache, "getUser").resolves({name: "brian", authToken: "abc-123"}) + sinon.spy(cache, "removeUser") user.logOut().then -> expect(cache.removeUser).to.be.calledOnce it "does not send to api.createSignout without a authToken", -> - @sandbox.spy(api, "createSignout") - @sandbox.stub(cache, "getUser").resolves({name: "brian"}) - @sandbox.spy(cache, "removeUser") + sinon.spy(api, "createSignout") + sinon.stub(cache, "getUser").resolves({name: "brian"}) + sinon.spy(cache, "removeUser") user.logOut().then -> expect(api.createSignout).not.to.be.called expect(cache.removeUser).to.be.calledOnce it "removes the session from cache even if api.createSignout rejects", -> - @sandbox.stub(api, "createSignout").withArgs("abc-123").rejects(new Error("ECONNREFUSED")) - @sandbox.stub(cache, "getUser").resolves({name: "brian", authToken: "abc-123"}) - @sandbox.spy(cache, "removeUser") + sinon.stub(api, "createSignout").withArgs("abc-123").rejects(new Error("ECONNREFUSED")) + sinon.stub(cache, "getUser").resolves({name: "brian", authToken: "abc-123"}) + sinon.spy(cache, "removeUser") user.logOut().catch -> expect(cache.removeUser).to.be.calledOnce context ".getLoginUrl", -> it "calls api.getLoginUrl", -> - @sandbox.stub(api, "getLoginUrl").resolves("https://github.com/login") + sinon.stub(api, "getLoginUrl").resolves("https://github.com/login") user.getLoginUrl().then (url) -> expect(url).to.eq("https://github.com/login") context ".ensureAuthToken", -> it "returns authToken", -> - @sandbox.stub(cache, "getUser").resolves({name: "brian", authToken: "abc-123"}) + sinon.stub(cache, "getUser").resolves({name: "brian", authToken: "abc-123"}) user.ensureAuthToken().then (st) -> expect(st).to.eq("abc-123") it "throws NOT_LOGGED_IN when no authToken, tagged as api error", -> - @sandbox.stub(cache, "getUser").resolves(null) + sinon.stub(cache, "getUser").resolves(null) user.ensureAuthToken() .then -> diff --git a/packages/server/test/unit/watchers_spec.coffee b/packages/server/test/unit/watchers_spec.coffee index ac0e1b0dcfa3..ee54e0bd3432 100644 --- a/packages/server/test/unit/watchers_spec.coffee +++ b/packages/server/test/unit/watchers_spec.coffee @@ -1,17 +1,18 @@ require("../spec_helper") -_ = require("lodash") +_ = require("lodash") chokidar = require("chokidar") +dependencyTree = require("dependency-tree") Watchers = require("#{root}lib/watchers") describe "lib/watchers", -> beforeEach -> - @standardWatcher = @sandbox.stub({ + @standardWatcher = sinon.stub({ on: -> close: -> }) - @sandbox.stub(chokidar, "watch").returns(@standardWatcher) + sinon.stub(chokidar, "watch").returns(@standardWatcher) @watchers = Watchers() it "returns instance of watcher class", -> @@ -28,12 +29,36 @@ describe "lib/watchers", -> expect(_.keys(@watchers.watchers)).to.have.length(1) expect(@watchers.watchers).to.have.property("/foo/bar") + context "#watchTree", -> + beforeEach -> + sinon.stub(dependencyTree, "toList").returns([ + "/foo/bar" + "/dep/a" + "/dep/b" + ]) + @watchers.watchTree("/foo/bar") + + it "watches each file in dependency tree", -> + expect(chokidar.watch).to.be.calledWith("/foo/bar") + expect(chokidar.watch).to.be.calledWith("/dep/a") + expect(chokidar.watch).to.be.calledWith("/dep/b") + + it "stores a reference to the watcher", -> + expect(_.keys(@watchers.watchers)).to.have.length(3) + expect(@watchers.watchers).to.have.property("/foo/bar") + expect(@watchers.watchers).to.have.property("/dep/a") + expect(@watchers.watchers).to.have.property("/dep/b") + + it "ignores node_modules", -> + expect(dependencyTree.toList.lastCall.args[0].filter("/foo/bar")).to.be.true + expect(dependencyTree.toList.lastCall.args[0].filter("/node_modules/foo")).to.be.false + context "#close", -> it "removes each watched property", -> - watched1 = {close: @sandbox.spy()} + watched1 = {close: sinon.spy()} @watchers._add("/one", watched1) - watched2 = {close: @sandbox.spy()} + watched2 = {close: sinon.spy()} @watchers._add("/two", watched2) expect(_.keys(@watchers.watchers)).to.have.length(2) diff --git a/scripts/binary/bump.coffee b/scripts/binary/bump.coffee index 7b268492ae78..19972d7e1060 100644 --- a/scripts/binary/bump.coffee +++ b/scripts/binary/bump.coffee @@ -173,7 +173,7 @@ module.exports = { console.log("setting environment variables in", project) car.updateProjectEnv(project, provider, { CYPRESS_NPM_PACKAGE_NAME: nameOrUrl, - CYPRESS_BINARY_VERSION: binaryVersionOrUrl + CYPRESS_INSTALL_BINARY: binaryVersionOrUrl }) awaitEachProjectAndProvider(PROJECTS, updateProject, projectFilter) .then R.always(result) diff --git a/scripts/run-cypress-tests.js b/scripts/run-cypress-tests.js index 897e412a9c94..0d08921cdb4a 100644 --- a/scripts/run-cypress-tests.js +++ b/scripts/run-cypress-tests.js @@ -12,6 +12,11 @@ const path = require('path') const minimist = require('minimist') const Promise = require('bluebird') const xvfb = require('../cli/lib/exec/xvfb') +const la = require('lazy-ass') +const is = require('check-more-types') +const fs = Promise.promisifyAll(require('fs-extra')) +const { existsSync } = require('fs') +const { basename } = require('path') const humanTime = require('../packages/server/lib/util/human_time.coffee') @@ -24,8 +29,81 @@ const started = new Date() let numFailed = 0 // turn this back on for driver + desktop gui tests +// TODO how does this work?! I don't see where the copying can possible happen process.env.COPY_CIRCLE_ARTIFACTS = 'true' +const isCircle = process.env.CI === 'true' && process.env.CIRCLECI === 'true' + +// matches the value in circle.yml "store_artifacts" command +const artifactsFolder = '/tmp/artifacts' + +// let us pass in project or resolve it to process.cwd() and dir +options.project = options.project || path.resolve(process.cwd(), options.dir || '') +console.log('options.project', options.project) + +const prepareArtifactsFolder = () => { + if (!isCircle) { + return Promise.resolve() + } + console.log('Making folder %s', artifactsFolder) + return fs.ensureDirAsync(artifactsFolder) +} + +const fileExists = (name) => Promise.resolve(existsSync(name)) + +const copyScreenshots = (name) => () => { + la(is.unemptyString(name), 'missing name', name) + + const screenshots = path.join(options.project, 'cypress', 'screenshots') + return fileExists(screenshots) + .then((exists) => { + if (!exists) { + return + } + + console.log('Copying screenshots for %s from %s', name, screenshots) + const destination = path.join(artifactsFolder, name) + + return fs.ensureDirAsync(destination) + .then(() => + fs.copyAsync(screenshots, destination, { + overwrite: true, + }) + ) + }) +} + +const copyVideos = (name) => () => { + const videos = path.join(options.project, 'cypress', 'videos') + return fileExists(videos) + .then((exists) => { + if (!exists) { + return + } + + console.log('Copying videos for %s from %s', name, videos) + const destination = path.join(artifactsFolder, name) + return fs.ensureDirAsync(destination) + .then(() => + fs.copyAsync(videos, destination, { + overwrite: true, + }) + ) + }) +} + +/** + * Copies artifacts (screenshots, videos) if configured into a subfolder + * + * @param {string} name Spec base name + */ +const copyArtifacts = (name) => () => { + if (!isCircle) { + return Promise.resolve() + } + return copyScreenshots(name)().then(copyVideos(name)) +} + function isLoadBalanced (options) { return _.isNumber(options.index) && _.isNumber(options.parallel) } @@ -43,12 +121,51 @@ _.defaults(options, { glob: 'cypress/integration/**/*', }) -// let us pass in project or resolve it to process.cwd() and dir -options.project = options.project || path.resolve(process.cwd(), options.dir) - // normalize and set to absolute path based on process.cwd options.glob = path.resolve(options.project, options.glob) +const runSpec = (spec) => { + console.log('\nRunning spec', spec) + la(is.unemptyString(spec), 'missing spec filename', spec) + + // get the path to xvfb-maybe binary + // const cmd = path.join(__dirname, '..', 'node_modules', '.bin', 'xvfb-maybe') + + const configFile = path.join(__dirname, '..', 'mocha-reporter-config.json') + + const args = [ + // '-as', + // '\"-screen 0 1280x720x16\"', + // '--', + // 'node', + path.resolve('..', '..', 'scripts', 'start.js'), // launch root monorepo start + '--run-project', + options.project, + '--spec', + spec, + '--reporter', + path.resolve(__dirname, '..', 'node_modules', 'mocha-multi-reporters'), + '--reporter-options', + `configFile=${configFile}`, + ] + + if (options.browser) { + args.push('--browser', options.browser) + } + + return spawn('node', args, { stdio: 'inherit' }) + .then((code) => { + console.log(`${spec} exited with code`, code) + + numFailed += code + }) + .then(copyArtifacts(basename(spec))) + .catch((err) => { + console.log(err) + throw err + }) +} + function run () { console.log('Specs found:') @@ -56,6 +173,7 @@ function run () { nodir: true, realpath: true, }) + .tap(prepareArtifactsFolder) .then((specs = []) => { if (options.spec) { return _.filter(specs, (spec) => { @@ -76,50 +194,12 @@ function run () { return specs }) .tap(console.log) - .each((spec = []) => { - console.log('\nRunning spec', spec) - - // get the path to xvfb-maybe binary - // const cmd = path.join(__dirname, '..', 'node_modules', '.bin', 'xvfb-maybe') - - const configFile = path.join(__dirname, '..', 'mocha-reporter-config.json') - - const args = [ - // '-as', - // '\"-screen 0 1280x720x16\"', - // '--', - // 'node', - path.resolve('..', '..', 'scripts', 'start.js'), // launch root monorepo start - '--run-project', - options.project, - '--spec', - spec, - '--reporter', - path.resolve(__dirname, '..', 'node_modules', 'mocha-multi-reporters'), - '--reporter-options', - `configFile=${configFile}`, - ] - - if (options.browser) { - args.push('--browser', options.browser) - } - - return spawn('node', args, { stdio: 'inherit' }) - .then((code) => { - console.log(`${spec} exited with code`, code) - - numFailed += code - }) - .catch((err) => { - console.log(err) - throw err - }) - }) + .each(runSpec) .then(() => { const duration = new Date() - started console.log('') - console.log('Total duration:', humanTime(duration)) + console.log('Total duration:', humanTime.long(duration)) console.log('Exiting with final code:', numFailed) process.exit(numFailed) diff --git a/scripts/test-other-projects.js b/scripts/test-other-projects.js index bb139cf40247..c2f63a3234ab 100644 --- a/scripts/test-other-projects.js +++ b/scripts/test-other-projects.js @@ -77,7 +77,7 @@ if (commitInfo) { // instructions for installing this binary // using https://github.com/bahmutov/commit-message-install const env = { - CYPRESS_BINARY_VERSION: binary, + CYPRESS_INSTALL_BINARY: binary, } const commitMessageInstructions = getInstallJson( npm, diff --git a/scripts/test-unique-npm-and-binary.js b/scripts/test-unique-npm-and-binary.js index 7234ec3a42e1..b82bce3270de 100644 --- a/scripts/test-unique-npm-and-binary.js +++ b/scripts/test-unique-npm-and-binary.js @@ -20,7 +20,7 @@ execa.shell(`npm install ${npm}`, { cwd, stdio: 'inherit', env: { - CYPRESS_BINARY_VERSION: binary, + CYPRESS_INSTALL_BINARY: binary, }, }) .then(console.log)