diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fd707be --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..c1c8f68 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,25 @@ + + +## Expected Behavior + + +## Actual Behavior + + +## Steps to Reproduce the Problem + + 1. + 2. + 3. + +## browserstack.json + + +## Platform details + + 1. browserstack-runner version: + 2. node version: + 3. os type and version: + +## Details + diff --git a/.github/workflows/Semgrep.yml b/.github/workflows/Semgrep.yml new file mode 100644 index 0000000..0347afd --- /dev/null +++ b/.github/workflows/Semgrep.yml @@ -0,0 +1,48 @@ +# Name of this GitHub Actions workflow. +name: Semgrep + +on: + # Scan changed files in PRs (diff-aware scanning): + # The branches below must be a subset of the branches above + pull_request: + branches: ["master", "main"] + push: + branches: ["master", "main"] + schedule: + - cron: '0 6 * * *' + + +permissions: + contents: read + +jobs: + semgrep: + # User definable name of this GitHub Actions job. + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + name: semgrep/ci + # If you are self-hosting, change the following `runs-on` value: + runs-on: ubuntu-latest + + container: + # A Docker image with Semgrep installed. Do not change this. + image: returntocorp/semgrep + + # Skip any PR created by dependabot to avoid permission issues: + if: (github.actor != 'dependabot[bot]') + + steps: + # Fetch project source with GitHub Actions Checkout. + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + # Run the "semgrep ci" command on the command line of the docker image. + - run: semgrep ci --sarif --output=semgrep.sarif + env: + # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable. + SEMGREP_RULES: p/default # more at semgrep.dev/explore + + - name: Upload SARIF file for GitHub Advanced Security Dashboard + uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 + with: + sarif_file: semgrep.sarif + if: always() \ No newline at end of file diff --git a/.gitignore b/.gitignore index bffa194..35e66d1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ node_modules browserstack.json browserstack-runner.pid lib/BrowserStackLocal +tests/jasmine +tests/jasmine2 +tests/mocha +tests/qunit diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..ff347da --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "boss": true, + "curly": true, + "eqeqeq": true, + "eqnull": true, + "expr": true, + "immed": true, + "noarg": true, + "node": true, + "quotmark": "single", + "smarttabs": true, + "sub": true, + "trailing": true, + "undef": true, + "unused": true, + "predef": [ + "require", + "global", + "window" + ] +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ba98975 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +.editorconfig +.jshintrc +tests diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..45dbde0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: node_js + +node_js: + - 'stable' + +before_install: + - npm install -g grunt-cli jshint gulp + +script: + - npm run-script test-ci + +cache: + directories: + - node_modules diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..c9eea17 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @browserstack/automate-public-repos diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b47ae28 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +## Contributing to browserstack-runner + +Your help improving this project is welcome! + +## Got a question or problem? Found an issue? + +If you have questions about how to use this tool, or you found a bug in the source code or a mistake in the documentation, [file an issue](https://github.com/browserstack/browserstack-runner/issues/new). + +## Want to contribute code? + +If you found an issue and want to contribute a fix or implement a new feature, send a pull request! + +To make changes: [Fork the repo, clone it locally](https://help.github.com/articles/fork-a-repo/), make a branch for your change, then implement it. + +Install some dependencies: + + npm install + +Then run tests with: + + npm test + +This runs some unit tests (consider adding more to `tests/unit`) and runs some other tools like jshint. Make sure to fix lint issues it finds. + +To test your change with another project where you use the tool, use `npm-link`: + + # in your browserstack-runner checkout + npm link + # in your project + npm link browserstack-runner + +Or do a local install: + + # in your project + npm install /path/to/checkout/browserstack-runner + +Once done, commit, push the branch to your repo and [create a pull request](https://help.github.com/articles/using-pull-requests/#initiating-the-pull-request). diff --git a/README.md b/README.md index 44771f1..269106c 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,279 @@ +# BrowserStack Runner + +[![Build Status](https://travis-ci.org/browserstack/browserstack-runner.svg?branch=master)](https://travis-ci.org/browserstack/browserstack-runner) + A command line interface to run browser tests over BrowserStack. -## Install -Go to the `browserstack-runner` directory. -Install browserstack-runner +## Usage - npm -g install +Install globally: -or + npm -g install browserstack-runner +Then, after setting up the configuration, run tests with: -For development, + browserstack-runner - npm link +You can also install locally and run the local binary: -## Configuration -To run browser tests on BrowserStack infrastructure, you need to -create a `browserstack.json` file in project's root directory (the -directory from which tests are run), by running this command: + npm install browserstack-runner + node_modules/.bin/browserstack-runner + +If you're getting an error `EACCES open ... BrowserStackLocal`, configure npm to install modules using something other than the default "nobody" user: + + npm -g config set user [user] + +Where `[user]` is replaced with a local user with enough permissions. + +CLI options: + +`--path`: Can be used if a different test runner is needed other than the one present in the `browserstack.json` file. + +`--pid`: Custom `pid` file that stores the pid's of the BrowserStackLocal instances created. + +`--verbose` or `-v`: For verbose logging. + +`--browsers` or `-b`: Space separated list of `cli_key` as defined in the `browserstack.json` file. This will run tests on the selected browsers only. If not present tests will run on all browsers present in the configuration file. + +Sample Usage: +`browserstack_runner --browsers 1 2 3 --path 'path/to/test/runner' --pid 'path/to/pid/file' -v` - browserstack-runner init [preset] +## Usage as a module -If nothing is provided as `preset` **default** is used. +`browserstack-runner` can also be used as a module. To run your tests, inside your project do - -> Currently only one preset is present: **default** +```node +var browserstackRunner = require("browserstack-runner"); + +var config = require("./browserstack.json"); + +browserstackRunner.run(config, function(error, report) { + if (error) { + console.log("Error:" + error); + return; + } + console.log(JSON.stringify(report, null, 2)); + console.log("Test Finished"); +}); +``` + +The callback to `browserstackRunner.run` is called with two params - + +- `error`: This parameter is either `null` or an `Error` object (if test execution failed) with message as the reason of why executing the tests on `BrowserStack` failed. +- `report`: This is an array which can be used to keep track of the executed tests and suites in a run. Each object in the array has the following keys - + - `browser`: The name of the browser the test executed on. + - `tests`: An array of `Test` objects. The `Test` Objects are described [here](https://github.com/js-reporters/js-reporters#event-data) + - `suites`: A global Suite Object as described [here](https://github.com/js-reporters/js-reporters#event-data) + +The structure of the `report` object is as follows - + +```json +[ + { + "browser": "Windows 7, Firefox 47.0", + "tests": [ + { + "name": "isOdd()", + "suiteName": "Odd Tests", + "fullName": ["Odd Tests", "isOdd()"], + "status": "passed", + "runtime": 2, + "errors": [], + "assertions": [ + { + "passed": true, + "actual": true, + "expected": true, + "message": "One is an odd number" + }, + { + "passed": true, + "actual": true, + "expected": true, + "message": "Three is an odd number" + }, + { + "passed": true, + "actual": true, + "expected": true, + "message": "Zero is not odd number" + } + ] + } + ], + "suites": { + "fullName": [], + "childSuites": [ + { + "name": "Odd Tests", + "fullName": ["Odd Tests"], + "childSuites": [], + "tests": [ + { + "name": "isOdd()", + "suiteName": "Odd Tests", + "fullName": ["Odd Tests", "isOdd()"], + "status": "passed", + "runtime": 2, + "errors": [], + "assertions": [ + { + "passed": true, + "actual": true, + "expected": true, + "message": "One is an odd number" + }, + { + "passed": true, + "actual": true, + "expected": true, + "message": "Three is an odd number" + }, + { + "passed": true, + "actual": true, + "expected": true, + "message": "Zero is not odd number" + } + ] + } + ], + "status": "passed", + "testCounts": { + "passed": 1, + "failed": 0, + "skipped": 0, + "total": 1 + }, + "runtime": 2 + } + ], + "tests": [], + "status": "passed", + "testCounts": { + "passed": 1, + "failed": 0, + "skipped": 0, + "total": 1 + }, + "runtime": 2 + } + } +] +``` -### Parameters +## Configuration - - *username*: BrowserStack username - (Alternatively: use `BROWSERSTACK_USERNAME` environment variable) +To run browser tests on BrowserStack infrastructure, you need to create a `browserstack.json` file in project's root directory (the directory from which tests are run), by running this command: - - *key*: BrowserStack key - (Alternatively: use `BROWSERSTACK_KEY` environment variable) +`browserstack-runner init [preset] [path]` - - *test_path*: Path to the which will execute the tests when opened - in a browser. +`preset`: Path of a custom preset file. Default: `presets/default.json` - - *test_framework*: Specify test framework which will execute the tests. - We support qunit, jasmine, jasmine 2.0 and mocha. +`path`: Path to test file. Default: `path/to/test/runner` - - *timeout*: Specify worker timeout with BrowserStack. +### Parameters for `browserstack.json` - - *browsers*: A list of browsers on which tests are to be run. +- `username`: BrowserStack username (Or `BROWSERSTACK_USERNAME` environment variable) +- `key`: BrowserStack [access key](https://www.browserstack.com/accounts/local-testing) (Or `BROWSERSTACK_KEY` environment variable) +- `test_path`: Path to the test page which will run the tests when opened in a browser. +- `test_framework`: Specify test framework which will run the tests. Currently supporting qunit, jasmine, jasmine1.3.1, jasmine2 and mocha. +- `test_server_port`: Specify test server port that will be opened from BrowserStack. If not set the default port 8888 will be used. Find a [list of all supported ports on browerstack.com](https://www.browserstack.com/question/664). +- `timeout`: Specify worker timeout with BrowserStack. +- `browsers`: A list of browsers on which tests are to be run. Find a [list of all supported browsers and platforms on browerstack.com](https://www.browserstack.com/list-of-browsers-and-platforms?product=js_testing). +- `build`: A string to identify your test run in Browserstack. In `TRAVIS` setup `TRAVIS_COMMIT` will be the default identifier. +- `proxy`: Specify a proxy to use for the local tunnel. Object with `host`, `port`, `username` and `password` properties. +- `exit_with_fail`: If set to true the cli process will exit with fail if any of the tests failed. Useful for automatic build systems. +- `tunnel_pid_file`: Specify a path to file to save the tunnel process id into. Can also by specified using the `--pid` flag while launching browserstack-runner from the command line. -A sample configuration file (list: http://www.browserstack.com/list-of-browsers-and-platforms?product=live): +A sample configuration file: ```json { "username": "", - "key": "", - "test_framework": "qunit/jasmine/jasmine2/mocha", + "key": "", + "test_framework": "qunit|jasmine|jasmine2|mocha", "test_path": ["relative/path/to/test/page1", "relative/path/to/test/page2"], - "browsers": [{ - "browser": "firefox", - "browser_version": "15.0", - "device": null, - "os": "OS X", - "os_version": "Snow Leopard" - }, - { - "browser": "firefox", - "browser_version": "16.0", - "device": null, - "os": "Windows", - "os_version": "7" - }, - { - "browser": "firefox", - "browser_version": "17.0", - "device": null, - "os": "Windows", - "os_version": "8" - }, - { - "browser": "ie", - "browser_version": "8.0", - "device": null, - "os": "Windows", - "os_version": "7" - }, + "test_server_port": "8899", + "browsers": [ + { + "browser": "ie", + "browser_version": "10.0", + "device": null, + "os": "Windows", + "os_version": "8", + "cli_key": 1 + }, + { + "os": "android", + "os_version": "4.0", + "device": "Samsung Galaxy Nexus", + "cli_key": 2 + }, + { + "os": "ios", + "os_version": "7.0", + "device": "iPhone 5S", + "cli_key": 3 + } + ] +} +``` + +#### `browsers` parameter + +`browsers` parameter is a list of objects, where each object contains the details of the browsers on which you want to run your tests. This object differs for browsers on desktop platforms and browsers on mobile platforms. Browsers on desktop platform should contain `browser`, `browser_version`, `os`, `os_version` parameters set as required and the `cli_key` parameter is optional and can be used in the command line when tests need to be run on a set of browsers from the `browserstack.json` file. + +Example: + +```json +{ + "browser": "ie", + "browser_version": "10.0", + "os": "Windows", + "os_version": "8", + "cli_key": 1 +} +``` + +For mobile platforms, `os`, `os_version` and `device` parameters are required. + +Example: + +```json +[ { - "browser": "ie", - "browser_version": "9.0", - "device": null, - "os": "Windows", - "os_version": "7" + "os": "ios", + "os_version": "8.3", + "device": "iPhone 6 Plus", + "cli_key": 1 }, { "os": "android", "os_version": "4.0", - "device": "Samsung Galaxy Nexus" - }, - { - "os": "ios", - "os_version": "7.0", - "device": "iPhone 5S" - }, - { - "browser": "ie", - "browser_version": "10.0", - "device": null, - "os": "Windows", - "os_version": "8" - }] -} + "device": "Google Nexus", + "cli_key": 2 + } +] ``` +For a full list of supported browsers, platforms and other details, [visit the BrowserStack site](https://www.browserstack.com/list-of-browsers-and-platforms?product=js_testing). + #### Compact `browsers` configuration -Alternatively, if `os` and `os_version` granularity is not desired, following configuration can be used: -- *browser*_current or *browser*_latest: will assign the latest version of the *browser*. -- *browser*_previous: will assign the previous version of the *browser*. -- *browser*_*version*: will assign the *version* specificed of the *browser*. Minor versions can be concatinated with underscore. +When `os` and `os_version` granularity is not desired, following configuration can be used: + +- `[browser]_current` or _browser_\_latest: will assign the latest version of the _browser_. +- `[browser]_previous`: will assign the previous version of the _browser_. +- `[browser]_[version]`: will assign the _version_ specified of the _browser_. Minor versions can be concatenated with underscores. + +This can also be mixed with fine-grained configuration. Example: + ```json -"browsers": [ +{ + "browsers": [ "chrome_previous", "chrome_latest", "firefox_previous", @@ -128,51 +287,72 @@ Example: "browser_version": "10.0", "device": null, "os": "Windows", - "os_version": "8" + "os_version": "8", + "cli_key": 1 } -] + ] +} ``` -### Enviroment variables - -* `BROWSERSTACK_USERNAME`: -BrowserStack user name. - -* `BROWSERSTACK_KEY`: -BrowserStack key. - -* `TUNNEL_ID`: -Identifier for the current instance of the tunnel process. In `TRAVIS` setup `TRAVIS_JOB_ID` will be the default identifier. - -* `BROWSERSTACK_JSON`: -Path to the browserstack.json file. If null, `browserstack.json` in the root directory will be used. +**Note:** +These shortcuts work only for browsers on desktop platforms supported by BrowserStack. ### Proxy support for BrowserStack local + Add the following in `browserstack.json` + ```json -... -"proxy": { +{ + "proxy": { "host": "localhost", "port": 3128, "username": "foo", "password": "bar" + } } -... ``` +### Supported environment variables + +To avoid duplication of system or user specific information across several configuration files, use these environment variables: + +- `BROWSERSTACK_USERNAME`: BrowserStack user name. +- `BROWSERSTACK_KEY`: BrowserStack key. +- `TUNNEL_ID`: Identifier for the current instance of the tunnel process. In `TRAVIS` setup `TRAVIS_JOB_ID` will be the default identifier. +- `BROWSERSTACK_JSON`: Path to the browserstack.json file. If null, `browserstack.json` in the root directory will be used. +- `BROWSERSTACK_LOCAL_BINARY_PATH`: Path to the browserstack local binary present on the system. If null, `BrowserStackLocal` in the `lib/` directory will be used. + ### Secure Information -To prevent checking in the BrowserStack `username` and `key` in your -source control, the corresponding environment variables can be used. +To avoid checking in the BrowserStack `username` and `key` in your source control system, the corresponding environment variables can be used. + +These can also be provided by a build server, for example [using secure environment variables on Travis CI](http://about.travis-ci.org/docs/user/build-configuration/#Secure-environment-variables). + +### Code Sample + +Check out code sample [here](https://github.com/browserstack/browserstack-runner-sample). + +### Running Tests + +BrowserStack Runner is currently tested by running test cases defined in [QUnit](https://github.com/jquery/qunit), [Mocha](https://github.com/mochajs/mocha), and [Spine](https://github.com/spine/spine) repositories. + +To run tests: + + npm test + +To run a larger suite of tests ensuring compatibility with older versions of QUnit, etc.: + + npm run test-ci + +Tests are also run for every pull request, courtesy [Travis CI](https://travis-ci.org/). + +### Timeout issue with Travis CI -The environment variables then can be safely provided in the build -configuration. For example, with travis-ci you can follow: +You might face [build timeout issue on Travis](https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received) if runner takes more than 10 minutes to run tests. -http://about.travis-ci.org/docs/user/build-configuration/#Secure-environment-variables +There are 2 possible ways to solve this problem: -## Running tests -Run `browserstack-runner` to run the tests on all the browsers mentioned -in the configuration. +1. Run a script which does `console.log` every 1-2 minutes. This will output to console and hence avoid Travis build timeout +2. Use `travis_wait` function provided by Travis-CI. You can prefix `browserstack-runner` command by `travis-wait` in your `travis.yml` file -You can include this in your test script to automatically run cross -browser tests on every build. +We would recommend using `travis_wait` function. It also allows you to configure wait time (ex: `travis_wait 20 browserstack-runner`, this will extend wait time to 20 minutes). Read more about `travis_wait` [here](https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received) diff --git a/bin/cli.js b/bin/cli.js index 97b89bf..5262736 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,61 +1,49 @@ -#! /usr/bin/env node - -todo = process.argv[2]; - -if (todo == '--verbose') - global.logLevel = "debug"; -else - global.logLevel = "info"; - - -if (todo == 'init') { - require('./init.js'); - return; -} else if(todo == '--version') { - require('./version.js'); - return; -} - var Log = require('../lib/logger'), - logger = new Log(global.logLevel), + logger = new Log(global.logLevel || 'info'), BrowserStack = require('browserstack'), - fs = require('fs'), + qs = require('querystring'), chalk = require('chalk'), - config = require('../lib/config'), utils = require('../lib/utils'), Server = require('../lib/server').Server, Tunnel = require('../lib/local').Tunnel, tunnel = require('tunnel'), http = require('http'), ConfigParser = require('../lib/configParser').ConfigParser, - serverPort = 8888, + config, + server, timeout, activityTimeout, + ackTimeout, + client, workers = {}, - browserToWorker = {}, workerKeys = {}, - logLevel, - tunnelingAgent, - tunnel; + tunnelingAgent; function terminateAllWorkers(callback) { + logger.trace('terminateAllWorkers'); + var cleanWorker = function(id, key) { + logger.trace('cleanWorker(%s, %s)', id, key); + client.terminateWorker(id, function() { var worker = workers[key]; if(worker) { logger.debug('[%s] Terminated', worker.string); + clearTimeout(worker.ackTimeout); clearTimeout(worker.activityTimeout); clearTimeout(worker.testActivityTimeout); delete workers[key]; delete workerKeys[worker.id]; } if (utils.objectSize(workers) === 0) { + logger.trace('terminateAllWorkers: done'); callback(); } }); }; if (utils.objectSize(workers) === 0) { + logger.trace('terminateAllWorkers: done'); callback(); } else { for (var key in workers){ @@ -65,74 +53,91 @@ function terminateAllWorkers(callback) { } else { delete workers[key]; if (utils.objectSize(workers) === 0) { + logger.trace('terminateAllWorkers: done'); callback(); } } } } -}; +} + +function cleanUpAndExit(signal, error, report, callback) { + ConfigParser.finalBrowsers = []; + callback = callback || function() {}; + report = report || []; + logger.trace('cleanUpAndExit: signal: %s', signal); -function cleanUpAndExit(signal, status) { try { server.close(); } catch (e) { - logger.debug("Server already closed"); + logger.debug('Server already closed'); } - if (statusPoller) statusPoller.stop(); - - try { - process.kill(tunnel.process.pid, 'SIGKILL'); - } catch (e) { - logger.debug("Non existent tunnel"); + if (statusPoller) { + statusPoller.stop(); } + try { - fs.unlinkSync(pid_file); + process.kill(tunnel.process.pid, 'SIGTERM'); } catch (e) { - logger.debug("Non existent pid file."); + logger.debug('Non existent tunnel'); } - if (signal == 'SIGTERM') { - logger.info("Exiting"); - process.exit(status); + if (signal === 'SIGTERM') { + logger.debug('Exiting'); + callback(error, report); } else { terminateAllWorkers(function() { - logger.info("Exiting"); - process.exit(1); + logger.debug('Exiting'); + callback(error, report); }); } } function getTestBrowserInfo(browserString, path) { var info = browserString; - if(config.multipleTest) { - info += ", " + path; + if (config.multipleTest) { + info += ', ' + path; } + + logger.trace('getTestBrowserInfo(%s, %s): %s', browserString, path, info); return info; } -function launchServer() { - logger.debug("Launching server on port:", serverPort); +function buildTestUrl(test_path, worker_key, browser) { + var host; + if (browser.os.toLowerCase() === 'ios' ){ + host = 'bs-local.com'; + } else { + host = 'localhost'; + } + var url = 'http://'+host+':' + config.test_server_port + '/' + test_path; + var browser_string = utils.browserString(browser); + var querystring = qs.stringify({ + _worker_key: worker_key, + _browser_string: browser_string + }); - var server = new Server(client, workers); - server.listen(parseInt(serverPort, 10)); + url += ((url.indexOf('?') > 0) ? '&' : '?') + querystring; + logger.trace('buildTestUrl:', url); + return url; } -function launchBrowser(browser, path) { - var url = 'http://localhost:' + serverPort.toString() + '/' + path; - var browserString = utils.browserString(browser); - logger.debug("[%s] Launching", getTestBrowserInfo(browserString, path)); +function launchServer(config, callback) { + logger.trace('launchServer:', config.test_server_port); + logger.debug('Launching server on port:', config.test_server_port); - var key = utils.uuid(); + server = new Server(client, workers, config, callback); + server.listen(parseInt(config.test_server_port, 10)); +} - if (url.indexOf('?') > 0) { - url += '&'; - } else { - url += '?'; - } +function launchBrowser(browser, path) { + var key = utils.uuid(); + var browserString = utils.browserString(browser); + var browserInfo = getTestBrowserInfo(browserString, path); + logger.debug('[%s] Launching', browserInfo); - url += '_worker_key=' + key + '&_browser_string=' + browserString; - browser['url'] = url; + browser.url = buildTestUrl(path.replace(/\\/g, '/'), key, browser); if (config.project) { browser.project = config.project; @@ -142,48 +147,48 @@ function launchBrowser(browser, path) { } if(config.tunnelIdentifier) { - browser["tunnel_identifier"] = config.tunnelIdentifier; + browser['tunnel_identifier'] = config.tunnelIdentifier; } timeout = parseInt(config.timeout); - if(! isNaN(timeout)) { + if (!isNaN(timeout)) { browser.timeout = timeout; } else { timeout = 300; } activityTimeout = timeout - 10; + ackTimeout = parseInt(config.ackTimeout) || 60; + + logger.trace('[%s] client.createWorker', browserInfo, browser); client.createWorker(browser, function (err, worker) { + logger.trace('[%s] client.createWorker | response:', browserInfo, worker, err); + if (err || typeof worker !== 'object') { - logger.info("Error from BrowserStack: ", err); - utils.alertBrowserStack("Failed to launch worker", - "Arguments: " + JSON.stringify({ - err: err, - worker: worker - }, null, 4)); + logger.info('Error from BrowserStack: ', err); return; } worker.config = browser; worker.string = browserString; worker.test_path = path; + worker.path_index = 0; + + // attach helper methods to manage worker state + attachWorkerHelpers(worker); + workers[key] = worker; workerKeys[worker.id] = {key: key, marked: false}; }); - } function launchBrowsers(config, browser) { setTimeout(function () { - if(Object.prototype.toString.call(config.test_path) === '[object Array]'){ + logger.trace('launchBrowsers', browser); + + if (Array.isArray(config.test_path)){ config.multipleTest = config.test_path.length > 1? true : false; - if(config.reuseWorker) { - launchBrowser(browser, config.test_path[0]); - } else { - config.test_path.forEach(function(path){ - launchBrowser(browser, path); - }); - } + launchBrowser(browser, config.test_path[0]); } else { config.multipleTest = false; launchBrowser(browser, config.test_path); @@ -191,132 +196,245 @@ function launchBrowsers(config, browser) { }, 100); } +function attachWorkerHelpers(worker) { + // TODO: Consider creating instances of a proper 'Worker' class + + worker.buildUrl = function buildUrl(test_path) { + var workerKey = workerKeys[this.id] ? workerKeys[this.id].key : null; + var url = buildTestUrl(test_path || this.test_path, workerKey, this.config); + logger.trace('[%s] worker.buildUrl: %s', this.id, url); + return url; + }; + + worker.getTestBrowserInfo = function getTestBrowserInfo(test_path) { + var info = this.string; + if (config.multipleTest) { + info += ', ' + (test_path || this.test_path); + } + return info; + }; + + worker.awaitAck = function awaitAck() { + var self = this; + + if (this.ackTimeout) { + logger.trace('[%s] worker.awaitAck: already awaiting ack, or awaited ack once and failed', self.id); + return; + } + + logger.trace('[%s] worker.awaitAck: timeout in %d secs', self.id, ackTimeout); + + this.ackTimeout = setTimeout(function () { + if (self.isAckd) { + logger.trace('[%s] worker.awaitAck: already ackd', self.id); + return; + } + + var url = self.buildUrl(); + logger.trace('[%s] worker.awaitAck: client.changeUrl: %s', self.id, url); + + client.changeUrl(self.id, { url: url }, function (err, data) { + logger.trace('[%s] worker.awaitAck: client.changeUrl: %s | response:', self.id, url, data, err); + logger.debug('[%s] Sent Request to reload url', self.getTestBrowserInfo()); + }); + + }, ackTimeout * 1000); + + logger.debug('[%s] Awaiting ack', this.getTestBrowserInfo()); + }; + + worker.markAckd = function markAckd() { + this.resetAck(); + this.isAckd = true; + + logger.trace('[%s] worker.markAckd', this.id); + logger.debug('[%s] Received ack', this.getTestBrowserInfo()); + }; + + worker.resetAck = function resetAck() { + logger.trace('[%s] worker.resetAck', this.id); + + clearTimeout(this.ackTimeout); + this.ackTimeout = null; + this.isAckd = false; + }; + + return worker; +} + var statusPoller = { poller: null, - start: function() { + start: function(callback) { + logger.trace('statusPoller.start'); + statusPoller.poller = setInterval(function () { client.getWorkers(function (err, _workers) { - _workers = _workers.filter(function(currentValue, index, array) { - return currentValue.status == 'running' && workerKeys[currentValue.id] && !workerKeys[currentValue.id].marked; - }); - for (var i in _workers) { - var _worker = _workers[i]; + logger.trace('client.getWorkers | response: worker count: %d', (_workers || []).length, err); + + if (!_workers) { + logger.info(chalk.red('Error found: ' + err)); + return; + } + _workers.filter(function(currentValue) { + return currentValue.status === 'running' && workerKeys[currentValue.id] && !workerKeys[currentValue.id].marked; + }).forEach(function(_worker) { var workerData = workerKeys[_worker.id]; var worker = workers[workerData.key]; - if (worker.launched) { + if (!worker || worker.launched) { return; } if (_worker.status === 'running') { //clearInterval(statusPoller); - logger.debug('[%s] Launched', getTestBrowserInfo(worker.string, worker.test_path)); + logger.debug('[%s] Launched', worker.getTestBrowserInfo()); worker.launched = true; workerData.marked = true; + // Await ack from browser-worker + worker.awaitAck(); + logger.trace('[%s] worker.activityTimeout: timeout in %d secs', worker.id, activityTimeout); + worker.activityTimeout = setTimeout(function () { - if (!worker.acknowledged) { - var subject = "Worker inactive for too long: " + worker.string; - var content = "Worker details:\n" + JSON.stringify(worker.config, null, 4); - utils.alertBrowserStack(subject, content, null, function(){}); + if (!worker.isAckd) { + logger.trace('[%s] worker.activityTimeout', worker.id); + delete workers[workerData.key]; delete workerKeys[worker.id]; config.status += 1; if (utils.objectSize(workers) === 0) { - var color = config.status > 0 ? "red" : "green"; - logger.info(chalk[color]("All tests done, failures: %d."), config.status); + var color = config.status > 0 ? 'red' : 'green'; + logger.info(chalk[color]('All tests done, failures: %d.'), config.status); if (config.status > 0) { config.status = 1; } - process.kill(process.pid, 'SIGTERM'); + logger.trace('[%s] worker.activityTimeout: all tests done', worker.id, config.status && 'with failures'); + var testsFailedError = utils.createTestsFailedError(config); + if(server && server.reports) { + callback(testsFailedError, server.reports); + } else { + callback(testsFailedError, {}); + } } + } else { + logger.trace('[%s] worker.activityTimeout: already ackd', worker.id); } }, activityTimeout * 1000); + + logger.trace('[%s] worker.testActivityTimeout: timeout in %d secs', worker.id, activityTimeout); + worker.testActivityTimeout = setTimeout(function () { - if (worker.acknowledged) { - var subject = "Tests timed out on: " + worker.string; - var content = "Worker details:\n" + JSON.stringify(worker.config, null, 4); - utils.alertBrowserStack(subject, content, null, function(){}); + if (worker.isAckd) { + logger.trace('[%s] worker.testActivityTimeout', worker.id); + delete workers[workerData.key]; delete workerKeys[worker.id]; config.status += 1; if (utils.objectSize(workers) === 0) { - var color = config.status > 0 ? "red" : "green"; - logger.info(chalk[color]("All tests done, failures: %d."), config.status); + var color = config.status > 0 ? 'red' : 'green'; + logger.info(chalk[color]('All tests done, failures: %d.'), config.status); if (config.status > 0) { config.status = 1; } - process.kill(process.pid, 'SIGTERM'); + logger.trace('[%s] worker.testActivityTimeout: all tests done', worker.id, config.status && 'with failures'); + var testsFailedError = utils.createTestsFailedError(config); + if(server && server.reports) { + callback(testsFailedError, server.reports); + } else { + callback(testsFailedError, {}); + } } + } else { + logger.trace('[%s] worker.testActivityTimeout: not ackd', worker.id); } }, (activityTimeout * 1000)); } - } + }); }); }, 2000); }, stop: function() { + logger.trace('statusPoller.poller'); clearInterval(statusPoller.poller); } }; -function runTests() { +function runTests(config, callback) { + var runTestsCallback = function(error, report) { + ConfigParser.finalBrowsers = []; + callback(error, report); + }; if (config.proxy) { + logger.trace('runTests: with proxy', config.proxy); + tunnelingAgent = tunnel.httpOverHttp({ proxy: config.proxy }); var oldhttpreq = http.request; - http.request = function (options, callback) { + http.request = function (options, reqCallback) { options.agent = tunnelingAgent; - return oldhttpreq.call(null, options, callback); + return oldhttpreq.call(null, options, reqCallback); }; } if (config.browsers && config.browsers.length > 0) { ConfigParser.parse(client, config.browsers, function(browsers){ - launchServer(); - tunnel = new Tunnel(config.key, serverPort, config.tunnelIdentifier, function () { - statusPoller.start(); - var total_workers = config.browsers.length * (Object.prototype.toString.call(config.test_path) === '[object Array]' ? config.test_path.length : 1); - logger.info("Launching " + total_workers + " workers"); - browsers.forEach(function(browser) { - if (browser.browser_version === "latest") { - logger.debug("[%s] Finding version.", utils.browserString(browser)); - - client.getLatest(browser, function(err, version) { - logger.debug("[%s] Version is %s.", - utils.browserString(browser), version); - browser.browser_version = version; - // So that all latest logs come in together - launchBrowsers(config, browser); - }); - } else { - launchBrowsers(config, browser); - } - }); + launchServer(config, runTestsCallback); + + logger.trace('runTests: creating tunnel'); + tunnel = new Tunnel(config.key, config.test_server_port, config.tunnelIdentifier, config, function (err) { + if(err) { + cleanUpAndExit(null, err, [], callback); + } else { + logger.trace('runTests: created tunnel'); + + statusPoller.start(runTestsCallback); + var total_runs = config.browsers.length * (Array.isArray(config.test_path) ? config.test_path.length : 1); + logger.info('Launching ' + config.browsers.length + ' worker(s) for ' + total_runs + ' run(s).'); + browsers.forEach(function(browser) { + if (browser.browser_version === 'latest') { + logger.debug('[%s] Finding version.', utils.browserString(browser)); + logger.trace('runTests: client.getLatest'); + + client.getLatest(browser, function(err, version) { + logger.trace('runTests: client.getLatest | response:', version, err); + logger.debug('[%s] Version is %s.', + utils.browserString(browser), version); + browser.browser_version = version; + // So that all latest logs come in together + launchBrowsers(config, browser); + }); + } else { + launchBrowsers(config, browser); + } + }); + } }); }); } else { - launchServer(); + launchServer(config, callback); } } -try { - var client = BrowserStack.createClient({ - username: config.username, - password: config.key - }); - runTests(); - var pid_file = process.cwd() + '/browserstack-run.pid'; - fs.writeFileSync(pid_file, process.pid, 'utf-8') - process.on('SIGINT', function() { cleanUpAndExit('SIGINT', 1) }); - process.on('SIGTERM', function() { cleanUpAndExit('SIGTERM', config.status) }); -} catch (e) { - console.log(e); - console.log('Invalid command.'); -} +exports.run = function(userConfig, callback) { + callback = callback || function() {}; + + try { + config = new (require('../lib/config').config)(userConfig); + + client = BrowserStack.createClient({ + username: config.username, + password: config.key + }); + runTests(config, function(error, report) { + cleanUpAndExit('SIGTERM', error, report, callback); + }); + } catch (e) { + callback(e); + } +}; diff --git a/bin/init.js b/bin/init.js index d1a4227..2c0febb 100755 --- a/bin/init.js +++ b/bin/init.js @@ -1,19 +1,24 @@ #! /usr/bin/env node var fs = require('fs'); -var preset = process.argv[3] || 'default'; +var preset = require('./runner').preset; +var path = require('./runner').path; var browsers = require('../presets/' + preset + '.json'); var config = { username: 'BROWSERSTACK_USERNAME', key: 'BROWSERSTACK_KEY', - test_path: 'path/to/test/runner', + test_path: path || 'path/to/test/runner', browsers: browsers -} +}; var configString = JSON.stringify(config, null, 4); -fs.writeFile('browserstack.json', configString, function (err, written, buffer) { +fs.writeFile('browserstack.json', configString, function (err) { + if (err) { + console.log('Failed to generate `browserstack.json`', err); + return; + } console.log('Generated `browserstack.json` using preset "%s" having %d browsers.', preset, browsers.length); }); diff --git a/bin/runner.js b/bin/runner.js new file mode 100755 index 0000000..94fdec0 --- /dev/null +++ b/bin/runner.js @@ -0,0 +1,113 @@ +#! /usr/bin/env node + +var yargs = require('yargs') + .command('init [preset] [path]', 'initialise browserstack.json with preset and test runner path', function(yargs) { + return yargs.option('preset', { + type: 'string', + default: 'default', + description: 'name of preset json file(without extension)(present in node_modules/browserstack-runner/presets to be used while initiating' + }) + .option('path', { + type: 'string', + default: '/path/to/test/runner', + description: 'path to test runner to be inserted in browserstack.json' + }); + }) + .option('browsers', { + alias: 'b', + type: 'array', + description: 'list of space separatedbrowsers keys as described in json file' + }) + .option('path', { + type: 'string', + description: 'path to test file' + }) + .option('version', { + alias: 'V', + description: 'browserstack-runner version' + }) + .option('pid', { + type: 'string', + description: 'path to pid file' + }) + .option('verbose', { + alias: 'v', + description: 'verbose logging' + }).argv; + +if (yargs['verbose']) { + global.logLevel = process.env.LOG_LEVEL || 'debug'; +} else { + global.logLevel = 'info'; +} +var path = require('path'), + config; + +if(yargs['_'].indexOf('init') !== -1) { + module.exports.preset = yargs['preset']; + module.exports.path = yargs['path']; + require('./init.js'); + return; +} + +var config_path = process.env.BROWSERSTACK_JSON || 'browserstack.json'; +config_path = path.resolve(path.relative(process.cwd(), config_path)); + +console.log('Using config:', config_path); +try { + config = require(config_path); +} catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + console.error('Configuration file `browserstack.json` is missing.'); + throw new Error('Configuration file `browserstack.json` is missing.'); + } else { + console.error('Invalid configuration in `browserstack.json` file'); + console.error(e.message); + console.error(e.stack); + throw new Error('Invalid configuration in `browserstack.json` file'); + } +} + +// extract a path to file to store tunnel pid +if(yargs.hasOwnProperty('pid')) { + if(yargs['pid'].trim().length > 0) { + config.tunnel_pid_file = yargs['pid'].trim(); + } else { + throw new Error('Empty pid file path'); + } +} + +// filter browsers according to from command line arguments +if(yargs['browsers']) { + if(yargs['browsers'].length > 0) { + config.browsers = config.browsers.filter( function(browser) { + return yargs['browsers'].indexOf(browser['cli_key']) !== -1; + }); + } else { + throw new Error('No browser keys specified. Usage --browsers ...'); + } + if(config.browsers.length === 0) { + throw new Error('Invalid browser keys'); + } + if(config.browsers.length < yargs['browsers'].length) { + console.warn('Some browser keys not present in config file.'); + } +} + +// test file path from cli arguments +config.test_path = yargs['path'] || config.test_path; + +var runner = require('./cli.js'); +runner.run(config, function(err) { + if(err) { + if (err.name === 'TestsFailedError') { + console.error('Exit with fail due to some tests failure.'); + } else { + console.error(err); + console.error(err.stack); + console.error('Invalid Command'); + } + process.exit(1); + } + process.exit(0); +}); diff --git a/bin/version.js b/bin/version.js index 506e70d..f3615d4 100644 --- a/bin/version.js +++ b/bin/version.js @@ -1,5 +1,5 @@ #! /usr/bin/env node -var packagePath = require('path').resolve(__dirname, "../package.json"), +var packagePath = require('path').resolve(__dirname, '../package.json'), packageJson = require(packagePath); -console.log("browserstack-runner @", packageJson["version"]); +console.log('browserstack-runner @', packageJson['version']); diff --git a/lib/_patch/browserstack-util.js b/lib/_patch/browserstack-util.js new file mode 100644 index 0000000..6821bf9 --- /dev/null +++ b/lib/_patch/browserstack-util.js @@ -0,0 +1 @@ +!function(t){function e(n){if(r[n])return r[n].exports;var o=r[n]={exports:{},id:n,loaded:!1};return t[n].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){!function(t){"use strict";t.BrowserStack=t.BrowserStack||{},t.BrowserStack.util={inspect:r(1),toArray:function(t,e){var r=[];e=e||0;for(var n=e||0;n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),i(e)?r.showHidden=e:e&&A(r,e),l(r.showHidden)&&(r.showHidden=!1),l(r.depth)&&(r.depth=2),l(r.colors)&&(r.colors=!1),l(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=u),w(r,t,r.depth)}function c(t,e){return t}function i(t){return"boolean"==typeof t}function l(t){return void 0===t}function u(t,e){var r=o.styles[e];return r?"["+o.colors[r][0]+"m"+t+"["+o.colors[r][1]+"m":t}function a(t){return"function"==typeof t}function f(t){return"string"==typeof t}function s(t){return"number"==typeof t}function p(t){return null===t}function y(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function g(t){return h(t)&&"[object RegExp]"===d(t)}function h(t){return"object"==typeof t&&null!==t}function b(t){return h(t)&&("[object Error]"===d(t)||t instanceof Error)}function v(t){return h(t)&&"[object Date]"===d(t)}function d(t){return Object.prototype.toString.call(t)}function j(t){var e={};return E(t,function(t,r){e[t]=!0}),e}function O(t,e,r,n,o){for(var c=[],i=0,l=e.length;l>i;++i)y(e,String(i))?c.push(m(t,e,r,n,String(i),!0)):c.push("");return E(o,function(o){o.match(/^\d+$/)||c.push(m(t,e,r,n,o,!0))}),c}function S(t){return"["+Error.prototype.toString.call(t)+"]"}function w(t,e,r){if(t.customInspect&&e&&a(e.inspect)&&e.inspect!==o&&(!e.constructor||e.constructor.prototype!==e)){var c=e.inspect(r,t);return f(c)||(c=w(t,c,r)),c}var i=C(t,e);if(i)return i;var l=n(e),u=j(l);if(t.showHidden&&Object.getOwnPropertyNames&&(l=Object.getOwnPropertyNames(e)),b(e)&&(_(l,"message")>=0||_(l,"description")>=0))return S(e);if(0===l.length){if(a(e)){var s=e.name?": "+e.name:"";return t.stylize("[Function"+s+"]","special")}if(g(e))return t.stylize(RegExp.prototype.toString.call(e),"regexp");if(v(e))return t.stylize(Date.prototype.toString.call(e),"date");if(b(e))return S(e)}var p="",y=!1,h=["{","}"];if(z(e)&&(y=!0,h=["[","]"]),a(e)){var d=e.name?": "+e.name:"";p=" [Function"+d+"]"}if(g(e)&&(p=" "+RegExp.prototype.toString.call(e)),v(e)&&(p=" "+Date.prototype.toUTCString.call(e)),b(e)&&(p=" "+S(e)),0===l.length&&(!y||0==e.length))return h[0]+p+h[1];if(0>r)return g(e)?t.stylize(RegExp.prototype.toString.call(e),"regexp"):t.stylize("[Object]","special");t.seen.push(e);var A;return A=y?O(t,e,r,u,l):T(l,function(n){return m(t,e,r,u,n,y)}),t.seen.pop(),x(A,p,h)}function m(t,e,r,n,o,c){var i,u,a;if(a={value:e[o]},Object.getOwnPropertyDescriptor&&(a=Object.getOwnPropertyDescriptor(e,o)||a),a.get?u=a.set?t.stylize("[Getter/Setter]","special"):t.stylize("[Getter]","special"):a.set&&(u=t.stylize("[Setter]","special")),y(n,o)||(i="["+o+"]"),u||(_(t.seen,a.value)<0?(u=p(r)?w(t,a.value,null):w(t,a.value,r-1),u.indexOf("\n")>-1&&(u=c?T(u.split("\n"),function(t){return" "+t}).join("\n").substr(2):"\n"+T(u.split("\n"),function(t){return" "+t}).join("\n"))):u=t.stylize("[Circular]","special")),l(i)){if(c&&o.match(/^\d+$/))return u;i=k.stringify(""+o),i.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(i=i.substr(1,i.length-2),i=t.stylize(i,"name")):(i=i.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),i=t.stylize(i,"string"))}return i+": "+u}function C(t,e){if(l(e))return t.stylize("undefined","undefined");if(f(e)){var r="'"+k.stringify(e).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return t.stylize(r,"string")}return s(e)?t.stylize(""+e,"number"):i(e)?t.stylize(""+e,"boolean"):p(e)?t.stylize("null","null"):void 0}function x(t,e,r){var n=0,o=N(t,function(t,e){return n++,e.indexOf("\n")>=0&&n++,t+e.replace(/\u001b\[\d\d?m/g,"").length+1},0);return o>60?r[0]+(""===e?"":e+"\n ")+" "+t.join(",\n ")+" "+r[1]:r[0]+e+" "+t.join(", ")+" "+r[1]}function A(t,e){if(!e||!h(e))return t;for(var r=n(e),o=r.length;o--;)t[r[o]]=e[r[o]];return t}var T=r(2),_=r(3),z=r(4),E=r(5),N=r(6),P=r(7),k=r(10);t.exports=o,o.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},o.styles={special:"cyan",number:"yellow","boolean":"yellow",undefined:"grey","null":"bold",string:"green",date:"magenta",regexp:"red"}},function(t,e){t.exports=function(t,e){if(t.map)return t.map(e);for(var n=[],o=0;oi;i++)e.call(o,t[i],i,t);else for(var l in t)r.call(t,l)&&e.call(o,t[l],l,t)}},function(t,e){var r=Object.prototype.hasOwnProperty;t.exports=function(t,e,n){var o=arguments.length>=3;if(o&&t.reduce)return t.reduce(e,n);if(t.reduce)return t.reduce(e);for(var c=0;c2?arguments[2]:null;if(l===+l)for(n=0;l>n;n++)null===u?e(i?t.charAt(n):t[n],n,t):e.call(u,i?t.charAt(n):t[n],n,t);else for(c in t)r.call(t,c)&&(null===u?e(t[c],c,t):e.call(u,t[c],c,t))}},function(t,e){"use strict";var r=Object.prototype.toString;t.exports=function n(t){var e=r.call(t),n="[object Arguments]"===e;return n||(n="[object Array]"!==e&&null!==t&&"object"==typeof t&&"number"==typeof t.length&&t.length>=0&&"[object Function]"===r.call(t.callee)),n}},function(t,e,r){var n;(function(o){!function(c){function i(t,e){function r(t){if(r[t]!==h)return r[t];var c;if("bug-string-char-index"==t)c="a"!="a"[0];else if("json"==t)c=r("json-stringify")&&r("json-parse");else{var i,l='{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}';if("json-stringify"==t){var a=e.stringify,f="function"==typeof a&&d;if(f){(i=function(){return 1}).toJSON=i;try{f="0"===a(0)&&"0"===a(new n)&&'""'==a(new o)&&a(v)===h&&a(h)===h&&a()===h&&"1"===a(i)&&"[1]"==a([i])&&"[null]"==a([h])&&"null"==a(null)&&"[null,null,null]"==a([h,v,null])&&a({a:[i,!0,!1,null,"\x00\b\n\f\r "]})==l&&"1"===a(null,i)&&"[\n 1,\n 2\n]"==a([1,2],null,1)&&'"-271821-04-20T00:00:00.000Z"'==a(new u(-864e13))&&'"+275760-09-13T00:00:00.000Z"'==a(new u(864e13))&&'"-000001-01-01T00:00:00.000Z"'==a(new u(-621987552e5))&&'"1969-12-31T23:59:59.999Z"'==a(new u(-1))}catch(s){f=!1}}c=f}if("json-parse"==t){var p=e.parse;if("function"==typeof p)try{if(0===p("0")&&!p(!1)){i=p(l);var y=5==i.a.length&&1===i.a[0];if(y){try{y=!p('" "')}catch(s){}if(y)try{y=1!==p("01")}catch(s){}if(y)try{y=1!==p("1.")}catch(s){}}}}catch(s){y=!1}c=y}}return r[t]=!!c}t||(t=c.Object()),e||(e=c.Object());var n=t.Number||c.Number,o=t.String||c.String,l=t.Object||c.Object,u=t.Date||c.Date,a=t.SyntaxError||c.SyntaxError,f=t.TypeError||c.TypeError,s=t.Math||c.Math,p=t.JSON||c.JSON;"object"==typeof p&&p&&(e.stringify=p.stringify,e.parse=p.parse);var y,g,h,b=l.prototype,v=b.toString,d=new u(-0xc782b5b800cec);try{d=-109252==d.getUTCFullYear()&&0===d.getUTCMonth()&&1===d.getUTCDate()&&10==d.getUTCHours()&&37==d.getUTCMinutes()&&6==d.getUTCSeconds()&&708==d.getUTCMilliseconds()}catch(j){}if(!r("json")){var O="[object Function]",S="[object Date]",w="[object Number]",m="[object String]",C="[object Array]",x="[object Boolean]",A=r("bug-string-char-index");if(!d)var T=s.floor,_=[0,31,59,90,120,151,181,212,243,273,304,334],z=function(t,e){return _[e]+365*(t-1970)+T((t-1969+(e=+(e>1)))/4)-T((t-1901+e)/100)+T((t-1601+e)/400)};(y=b.hasOwnProperty)||(y=function(t){var e,r={};return(r.__proto__=null,r.__proto__={toString:1},r).toString!=v?y=function(t){var e=this.__proto__,r=t in(this.__proto__=null,this);return this.__proto__=e,r}:(e=r.constructor,y=function(t){var r=(this.constructor||e).prototype;return t in this&&!(t in r&&this[t]===r[t])}),r=null,y.call(this,t)});var E={"boolean":1,number:1,string:1,undefined:1},N=function(t,e){var r=typeof t[e];return"object"==r?!!t[e]:!E[r]};if(g=function(t,e){var r,n,o,c=0;(r=function(){this.valueOf=0}).prototype.valueOf=0,n=new r;for(o in n)y.call(n,o)&&c++;return r=n=null,c?g=2==c?function(t,e){var r,n={},o=v.call(t)==O;for(r in t)o&&"prototype"==r||y.call(n,r)||!(n[r]=1)||!y.call(t,r)||e(r)}:function(t,e){var r,n,o=v.call(t)==O;for(r in t)o&&"prototype"==r||!y.call(t,r)||(n="constructor"===r)||e(r);(n||y.call(t,r="constructor"))&&e(r)}:(n=["valueOf","toString","toLocaleString","propertyIsEnumerable","isPrototypeOf","hasOwnProperty","constructor"],g=function(t,e){var r,o,c=v.call(t)==O,i=!c&&"function"!=typeof t.constructor&&N(t,"hasOwnProperty")?t.hasOwnProperty:y;for(r in t)c&&"prototype"==r||!i.call(t,r)||e(r);for(o=n.length;r=n[--o];i.call(t,r)&&e(r));}),g(t,e)},!r("json-stringify")){var P={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"},k="000000",U=function(t,e){return(k+(e||0)).slice(-t)},J="\\u00",D=function(t){for(var e='"',r=0,n=t.length,o=!A||n>10,c=o&&(A?t.split(""):t);n>r;r++){var i=t.charCodeAt(r);switch(i){case 8:case 9:case 10:case 12:case 13:case 34:case 92:e+=P[i];break;default:if(32>i){e+=J+U(2,i.toString(16));break}e+=o?c[r]:t.charAt(r)}}return e+'"'},F=function(t,e,r,n,o,c,i){var l,u,a,s,p,b,d,j,O,A,_,E,N,P,k,J;try{l=e[t]}catch(I){}if("object"==typeof l&&l)if(u=v.call(l),u!=S||y.call(l,"toJSON"))"function"==typeof l.toJSON&&(u!=w&&u!=m&&u!=C||y.call(l,"toJSON"))&&(l=l.toJSON(t));else if(l>-1/0&&1/0>l){if(z){for(p=T(l/864e5),a=T(p/365.2425)+1970-1;z(a+1,0)<=p;a++);for(s=T((p-z(a,0))/30.42);z(a,s+1)<=p;s++);p=1+p-z(a,s),b=(l%864e5+864e5)%864e5,d=T(b/36e5)%24,j=T(b/6e4)%60,O=T(b/1e3)%60,A=b%1e3}else a=l.getUTCFullYear(),s=l.getUTCMonth(),p=l.getUTCDate(),d=l.getUTCHours(),j=l.getUTCMinutes(),O=l.getUTCSeconds(),A=l.getUTCMilliseconds();l=(0>=a||a>=1e4?(0>a?"-":"+")+U(6,0>a?-a:a):U(4,a))+"-"+U(2,s+1)+"-"+U(2,p)+"T"+U(2,d)+":"+U(2,j)+":"+U(2,O)+"."+U(3,A)+"Z"}else l=null;if(r&&(l=r.call(e,t,l)),null===l)return"null";if(u=v.call(l),u==x)return""+l;if(u==w)return l>-1/0&&1/0>l?""+l:"null";if(u==m)return D(""+l);if("object"==typeof l){for(P=i.length;P--;)if(i[P]===l)throw f();if(i.push(l),_=[],k=c,c+=o,u==C){for(N=0,P=l.length;P>N;N++)E=F(N,l,r,n,o,c,i),_.push(E===h?"null":E);J=_.length?o?"[\n"+c+_.join(",\n"+c)+"\n"+k+"]":"["+_.join(",")+"]":"[]"}else g(n||l,function(t){var e=F(t,l,r,n,o,c,i);e!==h&&_.push(D(t)+":"+(o?" ":"")+e)}),J=_.length?o?"{\n"+c+_.join(",\n"+c)+"\n"+k+"}":"{"+_.join(",")+"}":"{}";return i.pop(),J}};e.stringify=function(t,e,r){var n,o,c,i;if("function"==typeof e||"object"==typeof e&&e)if((i=v.call(e))==O)o=e;else if(i==C){c={};for(var l,u=0,a=e.length;a>u;l=e[u++],i=v.call(l),(i==m||i==w)&&(c[l]=1));}if(r)if((i=v.call(r))==w){if((r-=r%1)>0)for(n="",r>10&&(r=10);n.lengthI;)switch(o=c.charCodeAt(I)){case 9:case 10:case 13:case 32:I++;break;case 123:case 125:case 91:case 93:case 58:case 44:return t=A?c.charAt(I):c[I],I++,t;case 34:for(t="@",I++;i>I;)if(o=c.charCodeAt(I),32>o)H();else if(92==o)switch(o=c.charCodeAt(++I)){case 92:case 34:case 47:case 98:case 116:case 110:case 102:case 114:t+=Z[o],I++;break;case 117:for(e=++I,r=I+4;r>I;I++)o=c.charCodeAt(I),o>=48&&57>=o||o>=97&&102>=o||o>=65&&70>=o||H();t+=$("0x"+c.slice(e,I));break;default:H()}else{if(34==o)break;for(o=c.charCodeAt(I),e=I;o>=32&&92!=o&&34!=o;)o=c.charCodeAt(++I);t+=c.slice(e,I)}if(34==c.charCodeAt(I))return I++,t;H();default:if(e=I,45==o&&(n=!0,o=c.charCodeAt(++I)),o>=48&&57>=o){for(48==o&&(o=c.charCodeAt(I+1),o>=48&&57>=o)&&H(),n=!1;i>I&&(o=c.charCodeAt(I),o>=48&&57>=o);I++);if(46==c.charCodeAt(I)){for(r=++I;i>r&&(o=c.charCodeAt(r),o>=48&&57>=o);r++);r==I&&H(),I=r}if(o=c.charCodeAt(I),101==o||69==o){for(o=c.charCodeAt(++I),43!=o&&45!=o||I++,r=I;i>r&&(o=c.charCodeAt(r),o>=48&&57>=o);r++);r==I&&H(),I=r}return+c.slice(e,I)}if(n&&H(),"true"==c.slice(I,I+4))return I+=4,!0;if("false"==c.slice(I,I+5))return I+=5,!1;if("null"==c.slice(I,I+4))return I+=4,null;H()}return"$"},B=function(t){var e,r;if("$"==t&&H(),"string"==typeof t){if("@"==(A?t.charAt(0):t[0]))return t.slice(1);if("["==t){for(e=[];t=R(),"]"!=t;r||(r=!0))r&&(","==t?(t=R(),"]"==t&&H()):H()),","==t&&H(),e.push(B(t));return e}if("{"==t){for(e={};t=R(),"}"!=t;r||(r=!0))r&&(","==t?(t=R(),"}"==t&&H()):H()),","!=t&&"string"==typeof t&&"@"==(A?t.charAt(0):t[0])&&":"==R()||H(),e[t.slice(1)]=B(R());return e}H()}return t},G=function(t,e,r){var n=L(t,e,r);n===h?delete t[e]:t[e]=n},L=function(t,e,r){var n,o=t[e];if("object"==typeof o&&o)if(v.call(o)==C)for(n=o.length;n--;)G(o,n,r);else g(o,function(t){G(o,t,r)});return r.call(t,e,o)};e.parse=function(t,e){var r,n;return I=0,M=""+t,r=B(R()),"$"!=R()&&H(),I=M=null,e&&v.call(e)==O?L((n={},n[""]=r,n),"",e):r}}}return e.runInContext=i,e}var l=r(11),u="object"==typeof o&&o;if(!u||u.global!==u&&u.window!==u||(c=u),"object"!=typeof e||!e||e.nodeType||l){var a=c.JSON,f=i(c,c.JSON3={noConflict:function(){return c.JSON=a,f}});c.JSON={parse:f.parse,stringify:f.stringify}}else i(c,e);l&&(n=function(){return f}.call(e,r,e,t),!(void 0!==n&&(t.exports=n)))}(this)}).call(e,function(){return this}())},function(t,e){(function(e){t.exports=e}).call(e,{})}]); \ No newline at end of file diff --git a/lib/_patch/browserstack.js b/lib/_patch/browserstack.js index 4080b60..a1fd54e 100644 --- a/lib/_patch/browserstack.js +++ b/lib/_patch/browserstack.js @@ -9,7 +9,7 @@ } // Tiny Ajax Post - var post = function (url, json, cb){ + var post = function (url, json, cb) { var req; if (window.ActiveXObject) @@ -20,30 +20,35 @@ throw "Strider: No ajax" req.onreadystatechange = function () { - if (req.readyState==4) - cb(req.responseText); - }; - var data = "data=" + encodeURIComponent(JSON.stringify(json)); + if (req.readyState==4) + cb(req.responseText); + }; + var data; + if(window.CircularJSON) { + data = window.CircularJSON.stringify(json); + } else { + data = JSON.stringify(json); + } req.open("POST", url, true); req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); req.setRequestHeader('X-Browser-String', BrowserStack.browser_string); req.setRequestHeader('X-Worker-UUID', BrowserStack.worker_uuid); - req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + req.setRequestHeader('Content-type', 'application/json'); req.send(data); } - if (typeof console !== 'object') { - var console = {}; - window.console = console; - } - - _console_log = console.log; - - console.log = function (arguments) { - post('/_log/', arguments, function () {}); + // Change some console method to capture the logs. + // This must not replace the console object itself so that other console methods + // from the browser or added by the tested application remain unaffected. + // https://github.com/browserstack/browserstack-runner/pull/199 + var browserstack_console = window.console || {}; + browserstack_console.log = function () { + var args = BrowserStack.util.toArray(arguments).map(BrowserStack.util.inspect); + post('/_log/', { arguments: args }, function () {}); }; - console.warn = function (arguments) { - post('/_log/', arguments, function () {}); + browserstack_console.warn = function () { + var args = BrowserStack.util.toArray(arguments).map(BrowserStack.util.inspect); + post('/_log/', { arguments: args }, function () {}); }; BrowserStack.post = post; @@ -53,4 +58,7 @@ BrowserStack.worker_uuid = getParameterByName('_worker_key'); window.BrowserStack = BrowserStack; + // If the browser didn't have a console object (old IE), then this will create it. + // Otherwise this is a no-op as it will assign the same object it already held. + window.console = browserstack_console; })(); diff --git a/lib/_patch/jasmine-jsreporter.js b/lib/_patch/jasmine-jsreporter.js index 67eeea3..76ccc0e 100644 --- a/lib/_patch/jasmine-jsreporter.js +++ b/lib/_patch/jasmine-jsreporter.js @@ -75,10 +75,7 @@ description : specs[i].description, durationSec : specs[i].durationSec, passed : specs[i].results().passedCount === specs[i].results().totalCount, - skipped : specs[i].results().skipped, - passedCount : specs[i].results().passedCount, - failedCount : specs[i].results().failedCount, - totalCount : specs[i].results().totalCount + results : specs[i].results() }; suiteData.passed = !suiteData.specs[i].passed ? false : suiteData.passed; suiteData.durationSec += suiteData.specs[i].durationSec; diff --git a/lib/_patch/jasmine-plugin.js b/lib/_patch/jasmine-plugin.js index c8abcfb..cd45520 100644 --- a/lib/_patch/jasmine-plugin.js +++ b/lib/_patch/jasmine-plugin.js @@ -1,39 +1,48 @@ (function(){ function countSpecs(suite, results){ - suite.specs.forEach(function(s){ - if(s.passed){ - results.passed++; - }else{ - results.tracebacks.push(s.description); - results.failed++; + for (var i = 0; i < suite.specs.length; ++i) { + if (suite.specs[i].passed){ + results.passed++; } - }); - suite.suites.forEach(function(s){ - results = countSpecs(s, results); - }); - return(results); + else if(suite.specs[i].results.skipped) { + results.skipped++; + } else { + results.failed++; + } + } + + for (var i = 0; i < suite.suites.length; ++i) { + if (suite.suites[i]) { + results = countSpecs(suite.suites[i], results); + } + } + + return results; } - var checker = setInterval(function(){ - if(!jasmine.running){ - var results = {} - var report = jasmine.getJSReport() - var errors = []; + var checker = setInterval(function() { + if (!jasmine.running) { + var results = {}; + var report = jasmine.getJSReport(); results.runtime = report.durationSec * 1000; - results.total=0; - results.passed=0; - results.failed=0; - results.tracebacks=[]; + results.total = 0; + results.passed = 0; + results.failed = 0; + results.skipped = 0; + + for (var i = 0; i < report.suites.length; ++i) { + if (report.suites[i]) { + results = countSpecs(report.suites[i], results); + } + } - jasmine.getJSReport().suites.forEach(function(suite){ - results = countSpecs(suite, results); - }); - results.total = results.passed + results.failed; + results.total = results.passed + results.failed + results.skipped; results.url = window.location.pathname; + results.report = report BrowserStack.post("/_report", results, function(){}); + clearInterval(checker); } - clearInterval(checker); }, 1000); })(); diff --git a/lib/_patch/jasmine2-plugin.js b/lib/_patch/jasmine2-plugin.js deleted file mode 100644 index 852d5d0..0000000 --- a/lib/_patch/jasmine2-plugin.js +++ /dev/null @@ -1,28 +0,0 @@ -(function() { - var checker = setInterval(function() { - if (!jasmine.running) { - var results = {}; - var specs = jsApiReporter.specs(); - results.runtime = jsApiReporter.executionTime(); - results.total = 0; - results.passed = 0; - results.failed = 0; - results.tracebacks = []; - - for (var spec in specs) { - if (specs[spec].status === 'passed') { - results.passed++; - } else { - results.tracebacks.push(specs[spec].description); - results.failed = true; - } - } - - results.total = results.passed + results.failed; - results.url = window.location.pathname; - BrowserStack.post('/_report', results, function(){}); - } - clearInterval(checker); - }, 1000); -})(); - diff --git a/lib/_patch/mocha-plugin.js b/lib/_patch/mocha-plugin.js deleted file mode 100644 index 1e84664..0000000 --- a/lib/_patch/mocha-plugin.js +++ /dev/null @@ -1,70 +0,0 @@ -(function() { - function stack(err) { - var str = err.stack || err.toString(); - - if (!~str.indexOf(err.message)) { - str = err.message + '\n' + str; - } - - if ('[object Error]' == str) { - str = err.message; - } - - if (!err.stack && err.sourceURL && err.line !== undefined) { - str += '\n(' + err.sourceURL + ':' + err.line + ')'; - } - return str.replace(/^/gm, ' '); - } - - function title(test) { - return test.fullTitle().replace(/#/g, ''); - } - - var origReporter = mocha._reporter; - - Mocha.BrowserStack = function(runner, root) { - origReporter.apply(this, arguments); - - var count = 1, - that = this, - failures = 0, - passes = 0, - start = 0, - tracebacks = []; - - runner.on('start', function() { - start = (new Date).getTime(); - }); - - runner.on('test end', function(test) { - count += 1; - }); - - runner.on('pass', function(test) { - passes += 1; - }); - - runner.on('fail', function(test, err) { - failures += 1; - - if (err) { - tracebacks.push(err); - } - }); - - runner.on('end', function() { - results = {}; - results.runtime = Date.now() - start; - results.total = passes + failures; - results.passed = passes; - results.failed = failures; - results.tracebacks = tracebacks; - results.url = window.location.pathname; - BrowserStack.post("/_report", results, function(){}); - }); - }; - - Mocha.BrowserStack.prototype = origReporter.prototype; - - return Mocha.BrowserStack; -})(); diff --git a/lib/_patch/qunit-plugin.js b/lib/_patch/qunit-plugin.js deleted file mode 100644 index 774ab1d..0000000 --- a/lib/_patch/qunit-plugin.js +++ /dev/null @@ -1,58 +0,0 @@ -// For logging assertions on the console, here's what grunt-contrib-qunit uses: -// https://github.com/gruntjs/grunt-contrib-qunit/blob/784597023e7235337ca9c0651aa45124a2d72341/tasks/qunit.js#L45 -(function() { - - var failedAssertions = []; - var options, - currentModule, - currentTest, - setTimeoutVariable; - var pendingTest = {}; - - var testTimeout = function() { - var error = { - testName: currentTest, - message: "Stuck on this test for 60 sec." - }; - - BrowserStack.post('/_progress', { - tracebacks: [error] - }, function(){}); - }; - - QUnit.testDone(function(details) { - var ct = details.module + " - " + details.name; - clearTimeout(pendingTest[ct]); - }); - - QUnit.testStart(function(details) { - currentTest = details.module + " - " + details.name; - pendingTest[currentTest] = setTimeout(function() { - testTimeout(currentTest); - }, 60000); - }); - - QUnit.log(function(details) { - if (details.result) { - return; - } - - var error = { - actual: details.actual, - expected: details.expected, - message: details.message, - source: details.source, - testName:( details.module + ": " + details.name) - }; - - BrowserStack.post('/_progress', { - tracebacks: [error] - }, function(){}); - }); - - QUnit.done(function(results) { - results.url = window.location.pathname; - BrowserStack.post("/_report", results, function(){}); - }); - -})(); diff --git a/lib/_patch/reporter.js b/lib/_patch/reporter.js new file mode 100644 index 0000000..64c79c8 --- /dev/null +++ b/lib/_patch/reporter.js @@ -0,0 +1,24 @@ +(function() { + var runner; + + if (window.QUnit) { + runner = new JsReporters.QUnitAdapter(QUnit); + } else if (window.jasmine) { + runner = new JsReporters.JasmineAdapter(jasmine.getEnv()); + } else if (window.mocha) { + runner = new JsReporters.MochaAdapter(mocha); + } else { + throw new Error('JsReporters: No testing framework was found'); + } + + runner.on('testEnd', function(eachTest) { + BrowserStack.post("/_progress", { + 'test': eachTest + }, function() {}); + }); + + runner.on('runEnd', function(globalSuite) { + BrowserStack.post("/_report", globalSuite, function() {}); + }); +})(); + diff --git a/lib/client-browserstack-util.js b/lib/client-browserstack-util.js new file mode 100644 index 0000000..3b8873d --- /dev/null +++ b/lib/client-browserstack-util.js @@ -0,0 +1,22 @@ +(function (global) { + 'use strict'; + + global.BrowserStack = global.BrowserStack || {}; + global.BrowserStack.util = { + inspect: require('util-inspect'), + toArray: function toArray(list, index) { + var array = []; + index = index || 0; + + for (var i = index || 0; i < list.length; i++) { + array[i - index] = list[i]; + } + + return array; + } + }; + + if (global.JSON3 && typeof global.JSON3.noConflict === 'function') { + global.JSON3.noConflict(); + } +})(window || {}); diff --git a/lib/config.js b/lib/config.js index 06115a0..0aa6d78 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,89 +1,88 @@ -var Log = require('./logger'), - logger = new Log(global.logLevel), - path = require('path'), - fs = require('fs'), +var fs = require('fs'), pwd = process.cwd(); -config_path = process.env.BROWSERSTACK_JSON || 'browserstack.json'; -config_path = path.resolve(path.relative(process.cwd(), config_path)); -logger.debug("Using config:", config_path); - -try { - var config = require(config_path); -} catch (e) { - if (e.code == 'MODULE_NOT_FOUND') { - logger.info('Configuration file `browserstack.json` is missing.'); - } else { - logger.info('Invalid configuration in `browserstack.json` file'); - logger.info(e.message); - logger.info(e.stack); +var formatPath = function(path) { + if (/^win/.test(process.platform)) { + path = path.replace(/\//g, '\\'); } - process.exit(1); -} - -try { - var package_json = require(process.cwd() + '/package.json'); -} catch (e) { - var package_json = {}; -} -if (process.env.BROWSERSTACK_KEY) { - config.key = process.env.BROWSERSTACK_KEY; -} + if (path.indexOf(pwd) === 0) { + path = path.slice(pwd.length + 1); + } + if (!fs.existsSync(path) && !fs.existsSync(path.split('?')[0])) { + throw new Error('Test path: ' + path + ' is invalid.'); + } + return path; +}; -if (process.env.BROWSERSTACK_USERNAME) { - config.username = process.env.BROWSERSTACK_USERNAME; -} +exports.config = function(config) { + var package_json = {}; + try { + package_json = require(process.cwd() + '/package.json'); + } catch (e) { + } -if (!config.project) { - var fallback_project; + if (process.env.BROWSERSTACK_KEY) { + this.key = process.env.BROWSERSTACK_KEY; + delete config.key; + } - if (config.username === 'OpensourceJSLib') { - fallback_project = 'Anonymous OpenSource Project'; + if (process.env.BROWSERSTACK_ACCESS_KEY) { + this.key = process.env.BROWSERSTACK_ACCESS_KEY; + delete config.key; } - config.project = process.env.TRAVIS_REPO_SLUG || package_json.name; -} + if (process.env.BROWSERSTACK_USERNAME) { + this.username = process.env.BROWSERSTACK_USERNAME; + delete config.username; + } -var commit_id = process.env.TRAVIS_COMMIT; + if (!config.project) { + var fallback_project; -if (commit_id) { - config.build = "Commit-" + commit_id.slice(0, commit_id.length / 2); -} + if (this.username === 'OpensourceJSLib') { + fallback_project = 'Anonymous OpenSource Project'; + } -['username', 'key', 'test_path', 'browsers'].forEach(function(param) { - if (typeof config[param] === 'undefined') { - console.error('Configuration parameter `%s` is required.', param); - process.exit(1); + this.project = process.env.TRAVIS_REPO_SLUG || fallback_project || package_json.name; } -}); -var formatPath = function(path) { - if (path.indexOf(pwd) === 0) { - path = path.slice(pwd.length + 1); - } - if (!fs.existsSync(path) && !fs.existsSync(path.split('?')[0])) { - console.error('Test path: ' + path + ' is invalid.'); - process.exit(1); - } - return path; -}; + var commit_id = process.env.TRAVIS_COMMIT; -config.tunnelIdentifier = process.env.TUNNEL_ID || process.env.TRAVIS_JOB_ID || process.env.TRAVIS_BUILD_ID; + if(!config.build) { + this.build = commit_id ? 'Commit-' + commit_id.slice(0, commit_id.length / 2) : 'Local run, ' + new Date().toISOString(); + } -if (Object.prototype.toString.call(config.test_path) === '[object Array]') { - config.test_path.forEach(function(path) { - path = formatPath(path); + var that = this; + ['username', 'key', 'browsers', 'test_path'].forEach(function(param) { + if (typeof config[param] === 'undefined' && typeof that[param] === 'undefined') { + throw new Error('Configuration parameter ' + param + ' is required.'); + } }); -} else { - //Backward Compatibility, if test_path is not array of path - config.test_path = formatPath(config.test_path); -} -config.status = 0; + this.tunnelIdentifier = process.env.TUNNEL_ID || process.env.TRAVIS_JOB_ID || process.env.TRAVIS_BUILD_ID; -for (var key in config) { - if (config.hasOwnProperty(key)) { - exports[key] = config[key]; + if (typeof(config['test_server']) === 'undefined') { + this.test_path = config.test_path; + if (Object.prototype.toString.call(this.test_path) === '[object Array]') { + this.test_path.forEach(function(path, index, test_path_array) { + test_path_array[index] = formatPath(path); + }); + + } else { + //Backward Compatibility, if test_path is not array of path + this.test_path = formatPath(this.test_path); + } + delete config.test_path; + } + + this.status = 0; + + for (var key in config) { + this[key] = config[key]; } -} + + if (!this.test_server_port) { + this.test_server_port = 8888; + } +}; diff --git a/lib/configParser.js b/lib/configParser.js index 4ab8fb9..b88995c 100644 --- a/lib/configParser.js +++ b/lib/configParser.js @@ -1,7 +1,9 @@ //beta browsers not handled //+ not handled var Log = require('./logger'), - logger = new Log(global.logLevel); + logger = new Log(global.logLevel || 'info'); + +var BROWSER_LIST_URL = 'https://www.browserstack.com/list-of-browsers-and-platforms/js_testing'; var ConfigParser = { finalBrowsers: [], @@ -11,9 +13,9 @@ var ConfigParser = { parse: function(client, browser_config, callback) { client.getBrowsers(function(error, browsers) { if(error) { - logger.info("Error getting browsers list from BrowserStack"); + logger.info('Error getting browsers list from BrowserStack'); logger.info(error); - process.exit(1); + throw new Error('Error getting browsers list from BrowserStack'); } ConfigParser.bsBrowsers = browsers; for (var key in browser_config) { @@ -22,12 +24,12 @@ var ConfigParser = { } callback(ConfigParser.finalBrowsers); }); - return + return; }, setBrowserVersion: function(browserObject, verStr) { - var filteredBrowsers = ConfigParser.bsBrowsers.map(function(currentValue, index, array) { - if (currentValue.browser.toLowerCase() == browserObject.browser) { + var filteredBrowsers = ConfigParser.bsBrowsers.map(function(currentValue) { + if (currentValue.browser.toLowerCase() === browserObject.browser) { return (browserObject.mobile ? currentValue.os_version : currentValue.browser_version); } }).filter(function(currentValue, index, array) { @@ -35,34 +37,50 @@ var ConfigParser = { }).sort(function(a, b) { return parseFloat(a) - parseFloat(b); }); - if (verStr == 'current' || verStr == 'latest') { - return filteredBrowsers[filteredBrowsers.length - 1]; + if (verStr === 'current' || verStr === 'latest') { + return ConfigParser.checkIfLatestFlagApplicable(browserObject) ? 'latest' : filteredBrowsers[filteredBrowsers.length - 1]; } - else if (verStr == 'previous') { - return filteredBrowsers[filteredBrowsers.length - 2]; + else if (verStr === 'previous') { + return ConfigParser.checkIfLatestFlagApplicable(browserObject) ? 'latest-1' : filteredBrowsers[filteredBrowsers.length - 2]; } }, + checkIfLatestFlagApplicable: function(browserObject) { + return !browserObject.mobile && browserObject.browser && ['chrome', 'firefox', 'edge'].includes(browserObject.browser.toLowerCase()); + }, + populateOsAndOsVersion: function(browserObject) { if (!(browserObject.os && browserObject.os_version)) { if (browserObject.mobile) { - var mobileFiltered = ConfigParser.bsBrowsers.filter(function(currentValue, index, array) { - return currentValue.browser.toLowerCase() == browserObject.browser && parseFloat(currentValue.os_version).toPrecision(4) == parseFloat(browserObject.os_version).toPrecision(4); + var mobileFiltered = ConfigParser.bsBrowsers.filter(function(currentValue) { + return currentValue.browser.toLowerCase() === browserObject.browser && parseFloat(currentValue.os_version).toPrecision(4) === parseFloat(browserObject.os_version).toPrecision(4); }); + if (!mobileFiltered.length) { + throw new Error('No mobile match found for ' + JSON.stringify(browserObject) + '\nCheck ' + BROWSER_LIST_URL); + } browserObject = mobileFiltered[Math.floor(Math.random() * mobileFiltered.length)]; - } - else { + } else { - var windowsFiltered = ConfigParser.bsBrowsers.filter(function(currentValue, index, array) { - return currentValue.os == 'Windows' && currentValue.browser == browserObject.browser && parseFloat(currentValue.browser_version).toPrecision(4) == parseFloat(browserObject.browser_version).toPrecision(4); + var windowsFiltered = ConfigParser.bsBrowsers.filter(function(currentValue) { + return currentValue.os === 'Windows' && currentValue.browser_version.match(/metro/i) == null && currentValue.browser === browserObject.browser && ((browserObject.browser_version && browserObject.browser_version.indexOf('latest') > -1) || parseFloat(currentValue.browser_version).toPrecision(4) === parseFloat(browserObject.browser_version).toPrecision(4)); }); - var osxFiltered = ConfigParser.bsBrowsers.filter(function(currentValue, index, array) { - return currentValue.os == 'OS X' && currentValue.browser == browserObject.browser && parseFloat(currentValue.browser_version).toPrecision(4) == parseFloat(browserObject.browser_version).toPrecision(4); + var osxFiltered = ConfigParser.bsBrowsers.filter(function(currentValue) { + return currentValue.os === 'OS X' && currentValue.browser === browserObject.browser && ((browserObject.browser_version && browserObject.browser_version.indexOf('latest')) > -1 || parseFloat(currentValue.browser_version).toPrecision(4) === parseFloat(browserObject.browser_version).toPrecision(4)); }); - browserObject = windowsFiltered.length > 0 ? windowsFiltered[Math.floor(Math.random() * windowsFiltered.length)] : osxFiltered[Math.floor(Math.random() * osxFiltered.length)]; + // Use Windows VMs if no OS specified + var desktopFiltered = windowsFiltered.length > 0 ? windowsFiltered : osxFiltered; + + if (!desktopFiltered.length) { + throw new Error('No desktop match found for ' + JSON.stringify(browserObject) + '\nCheck ' + BROWSER_LIST_URL); + } + var filteredObject = desktopFiltered[Math.floor(Math.random() * desktopFiltered.length)]; + if (browserObject.browser_version.indexOf('latest') > -1) { + filteredObject.browser_version = browserObject.browser_version; + } + browserObject = filteredObject; } } @@ -73,33 +91,30 @@ var ConfigParser = { var browserObject = {}; var version = null; var sliceStart = 1; - if (typeof(entry) == 'string') { - var browserData = entry.split("_"); + if (typeof entry === 'string') { + var browserData = entry.split('_'); var lindex = browserData.length - 1; - if (browserData[0] == 'mobile' || browserData[0] == 'android' || (browserData[0] == 'opera' && browserData[1] == 'browser')) { + if (browserData[0] === 'mobile' || browserData[0] === 'android' || (browserData[0] === 'opera' && browserData[1] === 'browser')) { browserObject.browser = browserData[0] + ' ' + browserData[1]; browserObject.mobile = true; sliceStart = 2; - } - else { + } else { browserObject.browser = browserData[0]; } - if (browserData[lindex] && browserData[lindex].indexOf("+") == -1) { - if (["current", "previous", "latest"].indexOf(browserData[lindex]) != -1) { + if (browserData[lindex] && browserData[lindex].indexOf('+') === -1) { + if (['current', 'previous', 'latest'].indexOf(browserData[lindex]) !== -1) { version = ConfigParser.setBrowserVersion(browserObject, browserData[lindex]); } else { - version = browserData.slice(sliceStart, lindex + 1).join("."); + version = browserData.slice(sliceStart, lindex + 1).join('.'); } - } - else { - version = browserData.slice(sliceStart, lindex + 1).join("."); + } else { + version = browserData.slice(sliceStart, lindex + 1).join('.'); } if (browserObject.mobile) { browserObject.os_version = version; browserObject.browser_version = null; - } - else { + } else { browserObject.browser_version = version; } } else { diff --git a/lib/local.js b/lib/local.js index 9b08830..f5fad2d 100644 --- a/lib/local.js +++ b/lib/local.js @@ -1,48 +1,45 @@ var Log = require('./logger'), - logger = new Log(global.logLevel), - exec = require('child_process').exec, + logger = new Log(global.logLevel || 'info'), + exec = require('child_process').execFile, fs = require('fs'), - http = require('http'), - windows = ((process.platform.match(/win32/) || process.platform.match(/win64/)) !== null), - localBinary = __dirname + (windows ? '/BrowserStackTunnel.jar' : '/BrowserStackLocal'), + path = require('path'), + https = require('https'), utils = require('./utils'), - config = require('./config'); + windows = ((process.platform.match(/win32/) || process.platform.match(/win64/)) !== null), + localBinary = __dirname + '/BrowserStackLocal' + (windows ? '.exe' : ''); -var Tunnel = function Tunnel(key, port, uniqueIdentifier, callback, err) { +var Tunnel = function Tunnel(key, port, uniqueIdentifier, config, callback) { var that = {}; + localBinary = process.env.BROWSERSTACK_LOCAL_BINARY_PATH || localBinary; + function tunnelLauncher() { - var tunnelCommand = (windows ? 'java -jar ' : '') + localBinary + ' '; - if (config.debug) tunnelCommand += ' -v '; - tunnelCommand += key + ' '; - tunnelCommand += 'localhost' + ','; - tunnelCommand += port.toString() + ','; - tunnelCommand += '0'; - tunnelCommand += (typeof uniqueIdentifier === 'undefined') ? ' -force -onlyAutomate' : ' -tunnelIdentifier ' + uniqueIdentifier; - tunnelCommand += checkAndAddProxy(); + var tunnelOptions = getTunnelOptions(key, uniqueIdentifier); if (typeof callback !== 'function') { callback = function(){}; } - logger.debug("[%s] Launching tunnel", new Date()); - var subProcess = exec(tunnelCommand, function(error, stdout, stderr) { + logger.debug('[%s] Launching tunnel', new Date()); + + var subProcess = exec(localBinary, tunnelOptions, function(error, stdout, stderr) { logger.debug(stderr); logger.debug(error); if (stdout.indexOf('Error') >= 0 || error) { - logger.debug("[%s] Tunnel launching failed", new Date()); + logger.debug('[%s] Tunnel launching failed', new Date()); logger.debug(stdout); - process.kill(process.pid, 'SIGINT'); + callback(new Error(new Date() + ': Tunnel launching failed')); } }); var data = ''; var running = false; - var runMatcher = "You can now access your local server(s)"; + var runMatchers = [ 'You can now access your local server(s)', 'Press Ctrl-C to exit' ]; setTimeout(function() { if (!running) { - utils.alertBrowserStack("Tunnel launch timeout", 'Stdout:\n' + data); + logger.error('BrowserStackLocal failed to launch within 30 seconds.'); + callback(new Error('BrowserStackLocal failed to launch within 30 seconds.')); } }, 30 * 1000); @@ -53,41 +50,140 @@ var Tunnel = function Tunnel(key, port, uniqueIdentifier, callback, err) { data += _data; - if (data.indexOf(runMatcher) >= 0) { + if (data.indexOf(runMatchers[0]) >= 0 && data.indexOf(runMatchers[1]) >= 0) { running = true; - logger.debug("[%s] Tunnel launched", new Date()); - callback(); + logger.debug('[%s] Tunnel launched', new Date()); + setTimeout(function(){ + callback(); + }, 2000); } }); + if (config.tunnel_pid_file) { + utils.mkdirp(path.dirname(config.tunnel_pid_file)); + fs.writeFile(config.tunnel_pid_file, subProcess.pid); + } + that.process = subProcess; } - function checkAndAddProxy() { + function getTunnelOptions(key, uniqueIdentifier) { + var options = [key]; + + if (config.debug) { + options.push('-v'); + } + + if (!uniqueIdentifier) { + options.push('-force'); + options.push('-onlyAutomate'); + } else { + options.push('-localIdentifier'); + options.push(uniqueIdentifier); + } + var proxy = config.proxy; - if(typeof proxy == 'undefined') { - return ""; + + if (proxy) { + options.push('-proxyHost ' + proxy.host); + options.push('-proxyPort ' + proxy.port); + + if (proxy.username && proxy.password) { + options.push('-proxyUser ' + proxy.username); + options.push('-proxyPass ' + proxy.password); + } } - var proxyCommand = ""; - proxyCommand += " -proxyHost " + proxy.host; - proxyCommand += " -proxyPort " + proxy.port; - if(typeof proxy.username !== 'undefined'){ - proxyCommand += " -proxyUser " + proxy.username; - proxyCommand += " -proxyPass " + proxy.password; + + return options; + } + + function runTunnelCmd(tunnelOptions, subProcessTimeout, processOutputHook, callback) { + var isRunning, subProcess, timeoutHandle; + + var callbackOnce = function (err, result) { + clearTimeout(timeoutHandle); + if (subProcess && isRunning) { + try { + process.kill(subProcess.pid, 'SIGKILL'); + subProcess = null; + } catch (e) { + logger.debug('[%s] failed to kill process:', new Date(), e); + } finally { + if (config.tunnel_pid_file) { + fs.unlink(config.tunnel_pid_file, function () {}); + } + } + } + + callback && callback(err, result); + callback = null; + }; + + isRunning = true; + + try { + subProcess = exec(localBinary, tunnelOptions, function (error, stdout) { + isRunning = false; + + if (error) { + callbackOnce(new Error('failed to get process output: ' + error)); + } else if (stdout) { + processOutputHook(stdout, callbackOnce); + } + }); + + subProcess.stdout.on('data', function (data) { + processOutputHook(data, callbackOnce); + }); + } catch (e) { + // Handles EACCESS and other errors when binary file exists, + // but doesn't have necessary permissions (among other issues) + callbackOnce(new Error('failed to get process output: ' + e)); + } + + if (subProcessTimeout > 0) { + timeoutHandle = setTimeout(function () { + callbackOnce(new Error('failed to get process output: command timeout')); + }, subProcessTimeout); } - return proxyCommand; } - fs.exists(localBinary, function(exists) { + function getTunnelBinaryVersion(callback) { + var subProcessTimeout = 3000; + + runTunnelCmd([ '--version' ], subProcessTimeout, function (data, done) { + var matches = /version\s+(\d+(\.\d+)*)/.exec(data); + var version = (matches && matches.length > 2) && matches[1]; + logger.debug('[%s] Tunnel binary: found version', new Date(), version); + + done(isFinite(version) ? null : new Error('failed to get binary version'), parseFloat(version)); + }, callback); + } + + function verifyTunnelBinary(callback) { + logger.debug('[%s] Verifying tunnel binary', new Date()); + + fs.exists(localBinary, function (exists) { + if (!exists) { + logger.debug('[%s] Verifying tunnel binary: file does not exist', new Date()); + callback(false); + } else { + getTunnelBinaryVersion(function (err, version) { + callback(!err && isFinite(version)); + }); + } + }); + } + + verifyTunnelBinary(function (exists) { if (exists) { tunnelLauncher(); return; } - logger.debug('Downloading BrowserStack Local to `%s`', localBinary); + logger.debug('Downloading BrowserStack Local to "%s"', localBinary); var file = fs.createWriteStream(localBinary); - var request = http.get( - (windows ? "http://www.browserstack.com/BrowserStackTunnel.jar" : ("http://s3.amazonaws.com/browserStack/browserstack-local/BrowserStackLocal-" + process.platform + "-" + process.arch)), + https.get('https://s3.amazonaws.com/browserStack/browserstack-local/BrowserStackLocal' + (windows ? '.exe' : '-' + process.platform + '-' + process.arch), function(response) { response.pipe(file); @@ -97,8 +193,8 @@ var Tunnel = function Tunnel(key, port, uniqueIdentifier, callback, err) { tunnelLauncher(); }, 100); }).on('error', function(e) { - logger.info("Got error while downloading binary: " + e.message); - process.kill(process.pid, 'SIGINT'); + logger.info('Got error while downloading binary: ' + e.message); + throw new Error('Got error while downloading binary: ' + e.message); }); }); }); diff --git a/lib/logger.js b/lib/logger.js index 1b43758..e387ab2 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,11 +1,13 @@ var fmt = require('util').format; -var logLevels = {ERROR: 3, INFO: 6, DEBUG: 7}; +var logLevels = { SILENT: 0, ERROR: 3, INFO: 6, DEBUG: 7, TRACE: 8 }; function Log(level){ - if ('string' == typeof level) level = logLevels[level.toUpperCase()]; + if ('string' === typeof level) { + level = logLevels[level.toUpperCase()]; + } this.level = isFinite(level) ? level : logLevels.DEBUG; this.stream = process.stdout; -}; +} Log.prototype = { @@ -17,16 +19,20 @@ Log.prototype = { } }, - error: function(msg){ + error: function(){ this.log('ERROR', arguments); }, - info: function(msg){ + info: function(){ this.log('INFO', arguments); }, - debug: function(msg){ + debug: function(){ this.log('DEBUG', arguments); + }, + + trace: function(){ + this.log('TRACE', arguments); } }; diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 0000000..020aee9 --- /dev/null +++ b/lib/proxy.js @@ -0,0 +1,33 @@ +var http = require('http'), + url = require('url'); + +var ProxyServer = { + onRequest: function(client_req, client_res, host, callback) { + var proxyUrl = url.parse(host); + var options = { + path: client_req.url, + hostname: proxyUrl.hostname, + port: proxyUrl.port, + method: client_req.method, + headers: client_req.headers + }; + + var proxy = http.request(options, function (res) { + var chunks = []; + res.on('data', function(chunk) { + chunks.push(chunk); + }); + res.on('end', function() { + //Replace + callback(res, Buffer.concat(chunks)); + }); + }).on('error', function(e) { + client_res.writeHead(500); + client_res.write('error: ' + e.toString()); + client_res.end(); + }); + proxy.end(); + } +}; + +exports.proxyServer = ProxyServer; diff --git a/lib/server.js b/lib/server.js index 6603f7d..6058ca1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,276 +1,537 @@ var Log = require('./logger'), - logger = new Log(global.logLevel), - http = require("http"), - url = require("url"), - path = require("path"), - fs = require("fs"), - qs = require("querystring"), - utils = require("./utils"), - config = require('../lib/config'), - exec = require('child_process').exec, - chalk = require('chalk'); - -var mimeTypes = { - "html": "text/html", - "json": "text/json", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "png": "image/png", - "js": "text/javascript", - "css": "text/css" -}; - -function getTestBrowserInfo(worker) { - var info = worker.string; - if(config.multipleTest) { - info += ", " + worker.test_path + logger = new Log(global.logLevel || 'info'), + http = require('http'), + url = require('url'), + path = require('path'), + util = require('util'), + fs = require('fs'), + qs = require('querystring'), + utils = require('./utils'), + proxyServer = require('./proxy').proxyServer, + chalk = require('chalk'), + mime = require('mime'), + send = require('send'), + vm = require('vm'), + CircularJSON = require('circular-json'), + resolve = require('resolve'), + zlib = require('zlib'); + +exports.Server = function Server(bsClient, workers, config, callback) { + var testFilePaths = (Array.isArray(config.test_path) ? config.test_path : [ config.test_path ]) + .map(function (path) { + return path.split(/[?#]/)[0]; + }), + reports = []; + + function getBrowserReport(browserInfo) { + var browserReport = null; + reports.forEach(function(report) { + if(report && report.browser === browserInfo) { + browserReport = report; + } + }); + if(!browserReport) { + browserReport = { + browser: browserInfo + }; + reports.push(browserReport); + } + browserReport.tests = browserReport.tests || []; + return browserReport; } - return info; -} + function reformatJasmineReport(browserReport) { + var results = browserReport.suites; + browserReport.tests = browserReport.tests || [ ]; + browserReport.suites = { + fullName : [ ], + childSuites : [ ], + tests : [ ], + status : !results.failed ? 'passed' : 'failed', + testCounts : { + passed : results.passed, + failed : results.failed, + skipped : results.skipped, + total : results.total, + }, + runtime : results.runtime + }; + function recurseThroughSuites(jasmineSuite, par) { + if(!jasmineSuite) { + return; + } + var suite = { + name : jasmineSuite.description, + fullName: [ ], + childSuites : [ ], + tests: [ ], + status : jasmineSuite.passed ? 'passed' : 'failed', + testCounts : { + passed : 0, + failed : 0, + skipped: 0, + total: 0 + }, + runtime: 0 + }; + if(par.name) { + suite.fullName.push(par.name); + } + suite.fullName.push(jasmineSuite.description); + jasmineSuite.specs.forEach(function(spec) { + var test = { + name : spec.description, + suiteName : suite.decription, + fullName : [ + ], + status : spec.passed ? 'passed' : (spec.results.skipped ? 'skipped' : 'failed'), + runtime : spec.durationSec, + errors : [ ], + assertions : [ ] + }; + Array.prototype.push.apply(test.fullName, suite.fullName); + test.fullName.push(spec.description); + if(!spec.passed) { + spec.results.items_.forEach(function(jasmineItem) { + if(!jasmineItem.passed_) { + var detail = { + passed : false + }; + if('message' in jasmineItem) { + detail.message = jasmineItem.message; + } + if('actual' in jasmineItem) { + detail.actual = jasmineItem.actual; + } + if('expected' in jasmineItem) { + detail.expected = jasmineItem.expected; + } + if('trace' in jasmineItem) { + detail.stack = jasmineItem.trace.message || jasmineItem.trace.stack; + } + test.errors.push(detail); + test.assertions.push(detail); + } + }); + } + suite.tests.push(test); + browserReport.tests.push(test); + if(spec.passed) { + ++suite.testCounts.passed; + } + else if(spec.skipped) { + ++suite.testCounts.skipped; + } + else { + ++suite.testCounts.failed; + } + ++suite.testCounts.total; + suite.runtime += spec.durationSec; + }); + jasmineSuite.suites.forEach(function(childSuite) { + recurseThroughSuites(childSuite, suite); + }); + par.childSuites.push(suite); + } + results.report.suites.forEach(function(jasmineSuite) { + recurseThroughSuites(jasmineSuite, browserReport.suites); + }); + } -exports.Server = function Server(bsClient, workers) { - - function handleFile(filename, request, response) { + function handleFile(filename, request, response, doNotUseProxy) { var url_parts = url.parse(request.url, true); var query = url_parts.query; if (query._worker_key && workers[query._worker_key]) { - worker = workers[query._worker_key] || {}; - worker.acknowledged = true; - logger.debug("[%s] Acknowledged", getTestBrowserInfo(worker)); + var worker = workers[query._worker_key]; + worker.markAckd(); } - fs.exists(filename, function(exists) { - if (!exists) { - response.writeHead(404, { - "Content-Type": "text/plain" - }); - response.write("404 Not Found\n"); - response.end(); - return; - } + var getReporterPatch = function () { + var scripts = [ + 'json2.js', + 'browserstack.js', + 'browserstack-util.js' + ]; - if (fs.lstatSync(filename).isDirectory()) { - filename = filename + (filename.lastIndexOf('/') == filename.length - 1 ? "" : "/") + "index.html"; - } + var framework_scripts = { + 'jasmine': ['jasmine-jsreporter.js', 'jasmine-plugin.js'] + }; - fs.readFile(filename, {encoding: 'utf8'}, function(err, file) { + var filePath = path.relative(process.cwd(), filename); + var pathMatches = (testFilePaths.indexOf(filePath) !== -1); - if (err) { - response.writeHead(500, { - "Content-Type": "text/plain" - }); - response.write(err + "\n"); - response.end(); - return; - } + if (pathMatches) { + var framework = config['test_framework']; + var tag_name = (framework === 'mocha') ? 'head' : 'body'; + var patch = '$1'; - var mimeType = mimeTypes[path.extname(filename).split(".")[1]]; - response.writeHead(200, { - "Content-Type": mimeType + "; charset=utf-8", + scripts.forEach(function(script) { + patch += '\n'; }); - scripts = [ - 'json2.js', - 'browserstack.js', - ]; + patch += externalScript('js-reporters/dist/js-reporters.js'); + patch += externalScript('circular-json/build/circular-json.js'); - framework_scripts = { - 'qunit': ['qunit-plugin.js'], - 'jasmine': ['jasmine-jsreporter.js', 'jasmine-plugin.js'], - 'jasmine2': ['jasmine2-plugin.js'], - 'mocha': ['mocha-plugin.js'] - }; - - var filePath = path.relative(process.cwd(), filename); - var pathMatches; - - if (typeof config.test_path === 'object') { - pathMatches = (config.test_path.indexOf(filePath) != -1); + // adding framework scripts + if (framework === 'jasmine') { + framework_scripts['jasmine'].forEach(function(script) { + patch += '\n'; + }); + patch += '\n'; } else { - pathMatches = (filePath == config.test_path); + patch += '\n'; } - if (pathMatches && mimeType === 'text/html') { - var framework = config['test_framework']; - var tag_name = (framework === "mocha") ? "head" : "body"; - var matcher = new RegExp("(.*)<\/" + tag_name + ">"); ///(.*)<\/body>/; - var patch = "$1"; - scripts.forEach(function(script) { - patch += "\n"; - }); + patch += ''; + return patch; + } + }; - // adding framework scripts - if (framework === "jasmine") { - framework_scripts['jasmine'].forEach(function(script) { - patch += "\n"; - }); - patch += "\n"; - } else if (framework === "jasmine2") { - framework_scripts['jasmine2'].forEach(function(script) { - patch += "\n"; - }); - } else if (framework === "mocha") { - framework_scripts['mocha'].forEach(function(script) { - patch += "\n"; + var getTestingFrameworkMatcher = function() { + var tag_name = (config['test_framework'] === 'mocha') ? 'head' : 'body'; + return new RegExp('(.*)<\/' + tag_name + '>'); ///(.*)<\/body>/; + }; + + var writeResponse = function(err, data) { + + if (err) { + sendError(response, err, 500); + return; + } + + response.writeHead(200, { + 'Content-Type': 'text/html; charset=utf-8' + }); + var matcher = getTestingFrameworkMatcher(); + var patch = getReporterPatch(); + data = data.replace(matcher, patch); + + response.write(data); + response.end(); + }; + + var patchResponse = function (data, headers, callback) { + var mimeType = mime.lookup(filename); + var finalData = data; + if (mimeType === 'text/html') { + var matcher = getTestingFrameworkMatcher(); + var patch = getReporterPatch(); + finalData = data.replace(matcher, patch); + headers['content-length'] = finalData.length; + } + callback && callback(finalData, headers); + }; + + var checkForEncodingAndPatch = function (responseData, headers, callback) { + var encoding = headers['content-encoding']; + if (encoding === 'gzip') { + zlib.gunzip(responseData, function (err, decoded) { + if (!err) { + patchResponse(decoded && decoded.toString(), headers, function (data, headers) { + zlib.gzip(data, function (err, encoded) { + if (!err) { + callback && callback(encoded, headers); + } + }); }); - patch += "\n"; - } else { - framework_scripts['qunit'].forEach(function(script) { - patch += "\n"; + } + }); + } else if (encoding === 'deflate') { + zlib.inflate(responseData, function (err, decoded) { + if (!err) { + patchResponse(decoded && decoded.toString(), headers, function (data, headers) { + zlib.deflate(data, function (err, encoded) { + if (!err) { + callback && callback(encoded, headers); + } + }); }); } - patch += ""; - - file = file.replace(matcher, patch); + }); + } else { + patchResponse(responseData && responseData.toString(), headers, function (data, headers) { + callback && callback(data, headers); + }); + } + }; + + if (!doNotUseProxy && config.test_server) { + proxyServer.onRequest(request, response, config.test_server, function (remote_response, response_data) { + var headers = remote_response.headers; + checkForEncodingAndPatch(response_data, headers, function (data, headers) { + response.writeHead(remote_response.statusCode, headers); + response.write(data); + response.end(); + }); + }); + } else { + fs.exists(filename, function(exists) { + if (!exists) { + sendError(response,'file not found', 404); + return; } + if (fs.lstatSync(filename).isDirectory()) { + filename = filename + (filename.lastIndexOf('/') === filename.length - 1 ? '' : '/') + 'index.html'; + } - response.write(file); - response.end(); + var mimeType = mime.lookup(filename); + if (mimeType === 'text/html') { + fs.readFile(filename, { encoding: 'utf8' }, function (err, data) { + writeResponse(err, data); + }); + } else { + send(request, filename) + .on('error', function onSendError(err) { + sendError(response, (err.message || 'Internal Server Error'), err.status || 500); + }) + .pipe(response); + } }); - }); - } - - function parseBody(body) { - // TODO: Have better implementation - return JSON.parse(qs.parse(body).data.escapeSpecialChars()); + } } function formatTraceback(details) { - // looks like QUnit data - if (details.testName) { - var output = "'" + details.testName + "' failed"; - if (details.message) { - output += ", " + details.message; + var output = '"' + details.testName + '" failed'; + if(details.error) { + if (details.error.message) { + output += ', ' + details.error.message; } - if (details.actual && details.expected) { - output += "\n" + chalk.blue("Expected: ") + details.expected + - "\n" + chalk.blue(" Actual: ") + details.actual; + if (details.error.actual != null && details.error.expected != null) { + output += '\n' + chalk.blue('Expected: ') + details.error.expected + + '\n' + chalk.blue(' Actual: ') + details.error.actual; } - if (details.source) { - output += "\n" + chalk.blue(" Source: ") + ""; - output += details.source.split("\n").join("\n\t "); + if (details.error.source || details.error.stack) { + output += '\n' + chalk.blue(' Source: ') + ''; + output += ( details.error.source || details.error.stack ).split('\n').join('\n\t '); } - return output; } - return details; + return output; } function checkAndTerminateWorker(worker, callback) { - if(config.reuseWorker && config.multipleTest) { - var next_path = getNextTestPath(worker.test_path); - var new_url = 'http://localhost:' + 8888 + '/' + next_path - + "_worker_key=" + worker._worker_key + "&_browser_string=" + getTestBrowserInfo(worker) ; - bsClient.postNewUrl(worker.id, {url: new_url}, function() { + var next_path = getNextTestPath(worker); + if (next_path) { + var url = worker.buildUrl(next_path); + worker.test_path = next_path; + worker.config.url = next_path; + + bsClient.changeUrl(worker.id, { url: url }, function () { callback(true); }); + } else { - bsClient.terminateWorker(worker.id, callback); + bsClient.terminateWorker(worker.id, function () { + callback(false); + }); } - }; + } - function getNextTestPath(test_path) { - // Someday I'll die for codes like these - for(var i = 0; i < config.test_path.length; i++) { - if(config.test_path[i] == test_path) { - return config.test_path[ i + 1 ]; - } + function getNextTestPath(worker) { + if (!config.multipleTest) { + return null; + } + return config.test_path[ ++worker.path_index ]; + } + + function getWorkerUuid(request) { + var uuid = request.headers['x-worker-uuid']; + + uuid = uuid && uuid.replace(/[^a-zA-Z0-9\-]/, ''); + logger.trace('cleaning up worker uuid:', uuid); + + uuid = (uuid && typeof workers[uuid] === 'object') ? uuid : null; + logger.trace('worker uuid', uuid, (uuid ? 'valid' : 'invalid')); + + return (uuid && workers[uuid]) ? uuid : null; + } + + + function sendError(response, errMessage, statusCode) { + response.writeHead(statusCode || 400, { + 'Content-Type': 'text/plain' + }); + + if (errMessage) { + response.write(errMessage + '\n'); } + response.end(); + } + + function externalScript(scriptPath) { + var resolvedPath = resolve.sync(scriptPath, { basedir: __dirname }); + var scriptContents = fs.readFileSync(resolvedPath, { encoding: 'utf8' }); + return ''; } - handlers = { - "_progress": function progressHandler(uri, body, request, response) { - var uuid = request.headers['x-worker-uuid']; - var worker = workers[uuid] || {}; - query = ""; + var handlers = { + '_progress': function progressHandler(uri, body, request, response) { + var uuid = getWorkerUuid(request); + + if (!uuid) { + sendError(response, 'worker not found', 404); + return; + } + + var worker = workers[uuid]; + var browserInfo = worker.getTestBrowserInfo(); + var query = null; + try { - query = parseBody(body); + query = CircularJSON.parse(body); } catch(e) { - logger.info("[%s] Exception in parsing log", worker.string); - logger.info("[%s] Log: " + qs.parse(body).data, worker.string); + logger.info('[%s] Exception in parsing log', worker.string); + logger.info('[%s] Log: ' + qs.parse(body).data, worker.string); } - if (query.tracebacks) { - query.tracebacks.forEach(function(traceback) { - logger.info(chalk.red("[%s] Error:"), getTestBrowserInfo(worker), formatTraceback(traceback)); + logger.trace('[%s] _progress', worker.id, CircularJSON.stringify(query)); + + if (query && query.test && query.test.errors) { + var browserReport = getBrowserReport(browserInfo); + browserReport.tests.push(query.test || {}); + + query.test.errors.forEach(function(error) { + logger.info('[%s] ' + chalk.red('Error:'), browserInfo, formatTraceback({ + error: error, + testName: query.test.name, + suiteName: query.test.suiteName + })); }); } response.end(); }, - "_report": function reportHandler(uri, body, request, response) { - query = null; + '_report': function reportHandler(uri, body, request, response) { + var uuid = getWorkerUuid(request); + if (!uuid) { + sendError(response, 'worker not found', 404); + return; + } + + + var worker = workers[uuid]; + worker._worker_key = uuid; + var browserInfo = worker.getTestBrowserInfo(); + + var query = null; try { - query = parseBody(body); + query = CircularJSON.parse(body); } catch (e) {} - var uuid = request.headers['x-worker-uuid']; - var worker = workers[uuid] || {}; - worker._worker_key = uuid; + + logger.trace('[%s] _report', worker.id, CircularJSON.stringify(query)); if (query === null) { - logger.info("[%s] Null response from remote Browser", request.headers['x-browser-string']); + logger.info('[%s] Null response from remote Browser', request.headers['x-browser-string']); } else { - if (query.tracebacks && query.tracebacks.length > 0) { - logger.info(chalk["red"]("[%s] Tracebacks:"), getTestBrowserInfo(worker)); - query.tracebacks.forEach(function(traceback) { - logger.info(traceback); - }); + var browserReport = getBrowserReport(browserInfo); + browserReport.suites = query; + + var color; + if(config['test_framework'] === 'jasmine') { + color = ( query.total !== query.passed ) ? 'red' : 'green'; + logger.info('[%s] ' + chalk[color](( query.total !== query.passed ) ? 'Failed:' : 'Passed:') + ' %d tests, %d passed, %d failed; ran for %dms', browserInfo, query.total, query.passed, query.failed, query.runtime); + config.status += query.failed; + reformatJasmineReport(browserReport); + } else if(query.testCounts) { + color = query.status === 'failed' ? 'red' : 'green'; + logger.info('[%s] ' + chalk[color](query.status === 'failed' ? 'Failed:' : 'Passed:') + ' %d tests, %d passed, %d failed, %d skipped; ran for %dms', browserInfo, query.testCounts.total, query.testCounts.passed, query.testCounts.failed, query.testCounts.skipped, query.runtime); + config.status += query.testCounts.failed; } - var color = query.failed ? "red" : "green"; - logger.info(chalk[color]("[%s] Completed in %d milliseconds. %d of %d passed, %d failed."), getTestBrowserInfo(worker), query.runtime, query.passed, query.total, query.failed); - config.status += query.failed; } - if (worker) { - bsClient.takeScreenshot(worker.id, function(error, screenshot) { - if (!error && screenshot.url) { - logger.info('[%s] ' + chalk['yellow']('Screenshot') + ': %s', getTestBrowserInfo(worker), screenshot.url); - } + logger.trace('[%s] _report: client.takeScreenshot', worker.id); - checkAndTerminateWorker(worker, function(reusedWorker) { - if (!workers[uuid]) { - return; - } + bsClient.takeScreenshot(worker.id, function(error, screenshot) { + logger.trace('[%s] _report: client.takeScreenshot | response:', worker.id, screenshot, error); - if(reusedWorker) { - logger.debug('[%s] Reused', getTestBrowserInfo(worker)); - return; - } + if (!error && screenshot.url && query && query.failed) { + logger.info('[%s] ' + chalk.yellow('Screenshot:') + ' %s', browserInfo, screenshot.url); + } - logger.debug('[%s] Terminated', getTestBrowserInfo(worker)); + checkAndTerminateWorker(worker, function(reusedWorker) { + if (!workers[uuid]) { + logger.trace('[%s] _report: checkAndTerminateWorker: worker not found', worker.id); + return; + } + + if (reusedWorker) { + logger.trace('[%s] _report: checkAndTerminateWorker: reused worker', worker.id); + logger.debug('[%s] Reused', browserInfo); + worker.resetAck(); + worker.awaitAck(); + return; + } - clearTimeout(workers[uuid].activityTimeout); - clearTimeout(workers[uuid].testActivityTimeout); - delete workers[uuid]; + logger.trace('[%s] _report: checkAndTerminateWorker: terminated', worker.id); + logger.debug('[%s] Terminated', browserInfo); - if (utils.objectSize(workers) === 0) { - var color = config.status > 0 ? "red" : "green"; - logger.info(chalk[color]("All tests done, failures: %d."), config.status); + clearTimeout(workers[uuid].ackTimeout); + clearTimeout(workers[uuid].activityTimeout); + clearTimeout(workers[uuid].testActivityTimeout); + delete workers[uuid]; - if (config.status > 0) { - config.status = 1; - } + if (utils.objectSize(workers) === 0) { + var color = config.status > 0 ? 'red' : 'green'; + logger.info(chalk[color]('All tests done, failures: %d.'), config.status); - process.kill(process.pid, 'SIGTERM'); + if (config.status > 0) { + config.status = 1; } - }); + + logger.trace('[%s] _report: checkAndTerminateWorker: all tests done', worker.id, config.status && 'with failures'); + var testsFailedError = utils.createTestsFailedError(config); + callback(testsFailedError, reports); + } }); - } + }); response.end(); }, - "_log": function logHandler(uri, body, request, response) { - query = parseBody(body); - logger.info('[' + request.headers['x-browser-string'] + '] ' + query); + '_log': function logHandler(uri, body, request, response) { + var uuid = getWorkerUuid(request); + var query = null; + try { + query = CircularJSON.parse(body); + } catch (e) { + query = body; + } + + logger.trace('[%s] _log', ((uuid && workers[uuid]) || {}).id, query); + + var logged = false; + + if (query && Array.isArray(query.arguments)) { + var context = { input: query.arguments, format: util.format, output: '' }; + var tryEvalOrString = 'function (arg) { try { return eval(\'o = \' + arg); } catch (e) { return arg; } }'; + + try { + // eval each element of query.arguments safely in an isolated context + vm.runInNewContext('output = format.apply(null, input.map(' + tryEvalOrString + '));', context); + logger.info('[' + request.headers['x-browser-string'] + '] ' + context.output); + logged = true; + } catch (e) { + logger.debug('_log: failed to format console log data', query); + } + } + + if (!logged) { + logger.info('[' + request.headers['x-browser-string'] + '] ' + query); + } + response.end(); }, - "_patch": function patchHandler(uri, body, request, response) { - handleFile(path.join(__dirname, uri), request, response); + '_patch': function patchHandler(uri, body, request, response) { + var filePath = path.join(__dirname, uri); + logger.trace('_patch', filePath); + + handleFile(filePath, request, response, true); }, - "_default": function defaultHandler(uri, body, request, response) { - handleFile(path.join(process.cwd(), uri), request, response); + '_default': function defaultHandler(uri, body, request, response) { + var filePath = path.join(process.cwd(), uri); + logger.trace('_default', filePath); + + handleFile(filePath, request, response); } }; @@ -278,8 +539,6 @@ exports.Server = function Server(bsClient, workers) { return http.createServer(function(request, response) { var uri = url.parse(request.url).pathname; var method = uri.split('/')[1]; - var filename; - var body = ''; request.on('data', function(data) { @@ -290,3 +549,5 @@ exports.Server = function Server(bsClient, workers) { }); }); }; + +exports.logger = logger; diff --git a/lib/utils.js b/lib/utils.js index 390f62a..79c02fd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,14 +1,15 @@ -var Log = require('./logger'), - logger = new Log(global.logLevel), - config = require('./config'), - http = require('http'), - url = require('url'), - querystring = require('querystring'); +var fs = require('fs'), + path = require('path'); String.prototype.escapeSpecialChars = function() { - return this.replace(/\n/g, "\\n") - .replace(/\\s/g, "\s") - .replace(/\\\'/, "\'"); + return this.replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/\f/g, '\\f') + .replace(/\u0008/g, '\\u0008') // \b + .replace(/\v/g, '\\u000b') // \v + .replace(/\0/g, '\\u0000') // \0 + .replace(/\\\'/, '\''); // TODO: check why this exists }; var titleCase = function toTitleCase(str) { @@ -20,7 +21,7 @@ var titleCase = function toTitleCase(str) { var uuid = function uuidGenerator() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, - v = c == 'x' ? r : (r & 0x3 | 0x8); + v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; @@ -28,9 +29,9 @@ var uuid = function uuidGenerator() { var browserString = function browserString(config) { var os_details = config.os + ' ' + config.os_version; if (config.browser) { - return os_details + ', ' + (config.browser == 'ie' ? 'Internet Explorer' : titleCase(config.browser)) + ' ' + config.browser_version; + return os_details + ', ' + (config.browser === 'ie' ? 'Internet Explorer' : titleCase(config.browser)) + ' ' + config.browser_version; } else { - return os_details + (config.device ? (', ' + config.device) : ""); + return os_details + (config.device ? (', ' + config.device) : ''); } }; @@ -38,84 +39,35 @@ var objectSize = function objectSize(obj) { var size = 0, key; for (key in obj) { - if (obj.hasOwnProperty(key)) size++; + if (obj.hasOwnProperty(key)) { + size++; + } } return size; }; -var alertBrowserStack = function alertBrowserStack(subject, content, params, fn) { - var endpoint = config.alert_endpoint || "http://www.browserstack.com/automate/alert"; - var urlObject = url.parse(endpoint); - - var context = config.alert_context || "Runner alert"; - logger.info("[%s] [%s] %s", new Date(), context, subject); - - if (typeof fn !== 'function') { - if (typeof params === 'function') { - } else { - fn = function() { - process.kill(process.pid, 'SIGINT'); - }; - } +var createTestsFailedError = function createTestsFailedError(config) { + var error = null; + if (config.status && config.exit_with_fail) { + error = new Error('Some tests failed.'); + error.name = 'TestsFailedError'; } + return error; +}; - if (!params || typeof(params) !== 'object') { - params = {}; +var mkdirp = function mkdirp(filepath) { + var dirname = path.dirname(filepath); + if (!fs.existsSync(dirname)) { + mkdirp(dirname); + } + if (!fs.existsSync(filepath)) { + fs.mkdirSync(filepath); } - - params.subject = subject; - params.content = content; - params.context = context; - - var body = querystring.stringify(params); - var options = { - hostname: urlObject.hostname, - port: urlObject.port, - path: urlObject.path, - method: 'POST', - auth: config.username + ":" + config.key, - headers: { - 'Content-Length': body.length - } - }; - - var callback = function(res) { - var response = ""; - res.setEncoding("utf8"); - res.on("data", function(chunk) { - response += chunk; - }); - res.on("end", function() { - if (res.statusCode !== 200) { - var message; - if (res.headers["content-type"].indexOf("json") !== -1) { - var resp = JSON.parse(response); - message = resp.message; - message += ' - ' + resp.errors.map(function(err) { - return '`' + err.field + '`' + ' ' + err.code; - }).join(', '); - } else { - message = response; - } - if (!message && res.statusCode === 403) { - message = "Forbidden"; - } - fn(new Error(message)); - } else { - fn(null, JSON.parse(response)); - } - }); - }; - - var request = http.request(options, callback); - request.write(body); - request.end(); - - return request; }; exports.titleCase = titleCase; exports.uuid = uuid; exports.browserString = browserString; exports.objectSize = objectSize; -exports.alertBrowserStack = alertBrowserStack; +exports.createTestsFailedError = createTestsFailedError; +exports.mkdirp = mkdirp; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ecd9f4e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1779 @@ +{ + "name": "browserstack-runner", + "version": "0.9.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=" + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + }, + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "available-typed-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", + "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==", + "dev": true, + "requires": { + "array-filter": "^1.0.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserstack": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.3.0.tgz", + "integrity": "sha1-hDgFPvasu4RNxrKRUQwZQznrUN8=" + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "optional": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true, + "optional": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "requires": { + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" + } + }, + "circular-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz", + "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=" + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true, + "optional": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "optional": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true, + "optional": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "optional": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + }, + "entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", + "dev": true + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true, + "optional": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "optional": true + }, + "extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "optional": true, + "requires": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "optional": true + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true, + "optional": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "optional": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "optional": true + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "optional": true, + "requires": { + "pend": "~1.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formatio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", + "dev": true, + "requires": { + "samsam": "~1.1" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true, + "optional": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "optional": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "dev": true, + "optional": true, + "requires": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true, + "optional": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typed-array": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", + "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.0", + "es-abstract": "^1.17.4", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true, + "optional": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "js-reporters": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/js-reporters/-/js-reporters-1.1.0.tgz", + "integrity": "sha1-yDwA/g1Mn2f5RLTt1fOylXSXzWI=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jshint": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.6.tgz", + "integrity": "sha512-KO9SIAKTlJQOM4lE64GQUtGBRpTOuvbrRrSZw3AhUxMNG266nX9hK2cKA4SBhXOj0irJGyNyGSLT62HGOVDEOA==", + "dev": true, + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "~4.17.10", + "minimatch": "~3.0.2", + "phantom": "~4.0.1", + "phantomjs-prebuilt": "~2.1.7", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x", + "unicode-5.2.0": "^0.7.5" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "optional": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", + "dev": true, + "optional": true + }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lolex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true, + "optional": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "optional": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "optional": true + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true, + "optional": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true, + "optional": true + }, + "phantom": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/phantom/-/phantom-4.0.12.tgz", + "integrity": "sha512-Tz82XhtPmwCk1FFPmecy7yRGZG2btpzY2KI9fcoPT7zT9det0CcMyfBFPp1S8DqzsnQnm8ZYEfdy528mwVtksA==", + "dev": true, + "optional": true, + "requires": { + "phantomjs-prebuilt": "^2.1.16", + "split": "^1.0.1", + "winston": "^2.4.0" + } + }, + "phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=", + "dev": true, + "optional": true, + "requires": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + } + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "optional": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "optional": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "optional": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true, + "optional": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true, + "optional": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true, + "optional": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "dev": true, + "optional": true, + "requires": { + "throttleit": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "samsam": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", + "dev": true + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + } + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true + }, + "sinon": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.5.tgz", + "integrity": "sha1-EDjLqDDjcBLpmmSDfs07ZyAMBYw=", + "dev": true, + "requires": { + "formatio": "1.1.1", + "lolex": "1.3.2", + "samsam": "1.1.2", + "util": ">=0.10.3 <1" + } + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "optional": true, + "requires": { + "through": "2" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true, + "optional": true + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=" + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true, + "optional": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true, + "optional": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "optional": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha1-6PmIEVynvp0HbHofrkeIvnCPDPE=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true, + "optional": true + }, + "unicode-5.2.0": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unicode-5.2.0/-/unicode-5.2.0-0.7.5.tgz", + "integrity": "sha512-KVGLW1Bri30x00yv4HNM8kBxoqFXr0Sbo55735nvrlsx4PYBZol3UtoWgO492fSwmsetzPEZzy73rbU8OGXJcA==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "optional": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.3.tgz", + "integrity": "sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true, + "optional": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "which-typed-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", + "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.2", + "es-abstract": "^1.17.5", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } + }, + "winston": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", + "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "dev": true, + "optional": true, + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "optional": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json index d49b4c3..276f7f2 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,27 @@ { "name": "browserstack-runner", "description": "A command line interface to run browser tests over BrowserStack", - "version": "0.1.14", + "version": "0.9.5", "homepage": "https://github.com/browserstack/browserstack-runner", "repository": { "type": "git", "url": "https://github.com/browserstack/browserstack-runner.git" }, "dependencies": { - "browserstack": "1.0.1", + "browserstack": "1.3.0", "chalk": "0.4.0", - "tunnel": "0.0.3" + "circular-json": "0.3.1", + "js-reporters": "1.1.0", + "mime": "1.6.0", + "resolve": "1.1.7", + "send": "0.16.2", + "tunnel": "0.0.3", + "yargs": "15.3.1" }, "devDependencies": { - "mocha": "~1.15.1" + "jshint": "2.9.6", + "mocha": "5.2.0", + "sinon": "1.17.5" }, "licenses": [ { @@ -22,6 +30,15 @@ } ], "bin": { - "browserstack-runner": "bin/cli.js" + "browserstack-runner": "bin/runner.js" + }, + "main": "bin/cli.js", + "scripts": { + "lint": "node_modules/.bin/jshint lib/*.js bin/ tests/*.js", + "test-unit": "node_modules/.bin/mocha tests/unit", + "test-behaviour": "node_modules/.bin/mocha tests/behaviour -R spec", + "test-ci": "npm run lint && npm run test-unit && npm run test-behaviour && TEST_MODE=all tests/external-tests.js", + "test": "npm run lint && npm run test-unit && npm run test-behaviour && TEST_MODE=required tests/external-tests.js", + "update-util": "webpack" } } diff --git a/presets/default.json b/presets/default.json index 5498339..dcfcf53 100644 --- a/presets/default.json +++ b/presets/default.json @@ -3,48 +3,56 @@ "browser": "firefox", "browser_version": "latest", "os": "OS X", - "os_version": "Lion" + "os_version": "Lion", + "cli_key": 1 }, { "browser": "safari", "browser_version": "latest", "os": "OS X", - "os_version": "Mountain Lion" + "os_version": "Mountain Lion", + "cli_key": 2 }, { "browser": "chrome", "browser_version": "latest", "os": "OS X", - "os_version": "Mountain Lion" + "os_version": "Mountain Lion", + "cli_key": 3 }, { "browser": "firefox", "browser_version": "latest", "os": "Windows", - "os_version": "7" + "os_version": "7", + "cli_key": 4 }, { "browser": "chrome", "browser_version": "latest", "os": "Windows", - "os_version": "7" + "os_version": "7", + "cli_key": 5 }, { "browser": "ie", "browser_version": "9.0", "os": "Windows", - "os_version": "7" + "os_version": "7", + "cli_key": 6 }, { "browser": "ie", "browser_version": "10.0", "os": "Windows", - "os_version": "8" + "os_version": "8", + "cli_key": 7 }, { "browser": "ie", "browser_version": "11.0", "os": "Windows", - "os_version": "7" + "os_version": "7", + "cli_key": 8 } ] diff --git a/tests/behaviour/resources/even.js b/tests/behaviour/resources/even.js new file mode 100644 index 0000000..adc9d50 --- /dev/null +++ b/tests/behaviour/resources/even.js @@ -0,0 +1,3 @@ +function isEven(val) { + return val % 2 === 0; +} diff --git a/tests/behaviour/resources/odd.js b/tests/behaviour/resources/odd.js new file mode 100644 index 0000000..3875785 --- /dev/null +++ b/tests/behaviour/resources/odd.js @@ -0,0 +1,3 @@ +function isOdd(val) { + return val % 2 === 1; +} diff --git a/tests/behaviour/resources/qunit_sample.html b/tests/behaviour/resources/qunit_sample.html new file mode 100644 index 0000000..5e1fce6 --- /dev/null +++ b/tests/behaviour/resources/qunit_sample.html @@ -0,0 +1,17 @@ + + + + +QUnit Example + + + + +
+
+ + + + + + diff --git a/tests/behaviour/resources/qunit_test1.js b/tests/behaviour/resources/qunit_test1.js new file mode 100644 index 0000000..f596e55 --- /dev/null +++ b/tests/behaviour/resources/qunit_test1.js @@ -0,0 +1,22 @@ +QUnit.module('Partial Tests', function() { + QUnit.test('console Tests', function(assert) { + // console functions should exist + assert.ok(typeof console.info === 'function', 'console.info exists'); + assert.ok(typeof console.warn === 'function', 'console.warn exists'); + assert.ok(typeof console.log === 'function', 'console.log exists'); + assert.ok(typeof console.error === 'function', 'console.error exists'); + assert.ok(typeof console.debug === 'function', 'console.debug exists'); + }); + + QUnit.test('Partial Tests', function(assert) { + // Fails + assert.ok(isOdd(2), '2 is an odd number'); + assert.ok(isEven(5), '5 is an even number'); + + // Passes + assert.ok(isOdd(3), '3 is an odd number'); + assert.ok(!isOdd(4), '4 is not an odd number'); + assert.ok(isEven(6), '6 is an even number'); + assert.ok(!isEven(7), '7 is not an even number'); + }); +}); diff --git a/tests/behaviour/resources/qunit_test2.js b/tests/behaviour/resources/qunit_test2.js new file mode 100644 index 0000000..e9a6e6e --- /dev/null +++ b/tests/behaviour/resources/qunit_test2.js @@ -0,0 +1,19 @@ +QUnit.module('All Pass/Fail tests', function() { + QUnit.test('All Pass', function(assert) { + assert.ok(isOdd(13), '13 is an odd number'); + assert.ok(isOdd(15), '15 is an odd number'); + assert.ok(!isOdd(12), '12 is not an odd number'); + assert.ok(isEven(14), '14 is an even number'); + assert.ok(isEven(16), '16 is an even number'); + assert.ok(!isEven(17), '17 is not an even number'); + }); + + QUnit.test('All Fail', function(assert) { + assert.ok(isOdd(22), '22 is an odd number'); + assert.ok(isOdd(24), '24 is an odd number'); + assert.ok(!isOdd(21), '21 is not an odd number'); + assert.ok(isEven(23), '23 is an even number'); + assert.ok(isEven(25), '25 is an even number'); + assert.ok(!isEven(26), '26 is not an even number'); + }); +}); diff --git a/tests/behaviour/runner.js b/tests/behaviour/runner.js new file mode 100644 index 0000000..7c898b1 --- /dev/null +++ b/tests/behaviour/runner.js @@ -0,0 +1,266 @@ +'use strict'; + +global.logLevel = 'silent'; + +var assert = require('assert'), + sinon = require('sinon'), + path = require('path'), + http = require('http'), + browserstackRunner = require('../../bin/cli.js'), + Tunnel = require('../../lib/local.js').Tunnel, + exec = require('child_process').exec, + execSync = require('child_process').execSync; + +var getBaseConfig = function() { + return { + username: 'BROWSERSTACK_USER', + key: 'BROWSERSTACK_KEY', + test_framework: 'qunit', + test_path: path.resolve(__dirname, 'resources', 'qunit_sample.html'), + build: 'BrowserStack Runner Behaviour Tests', + browsers: [ { + browser: 'firefox', + browser_version: '47.0', + os: 'Windows', + os_version: '7' + }, { + browser: 'chrome', + browser_version: '52.0', + os: 'Windows', + os_version: '7' + }, { + browser: 'iphone', + browser_version: '', + device: 'iPhone SE', + os: 'ios', + os_version: '11.2', + real_mobile: true + } ] + } +}; + +describe('Config Assertions', function() { + this.timeout(0); + + it('should run normally with valid config', function(done) { + browserstackRunner.run(getBaseConfig(), function(err) { + assert.equal(err, null); + done(); + }); + }); + it('should have an error if test path is not valid', function(done) { + var config = getBaseConfig(); + config.test_path = 'Some invalid path'; + browserstackRunner.run(config, function(err) { + assert.equal(err.message, 'Test path: ' + config.test_path + ' is invalid.'); + done(); + }); + }); + it('should have an error if config does not have a browsers key', function(done) { + var config = getBaseConfig(); + delete(config.browsers); + browserstackRunner.run(config, function(err) { + assert.equal(err.message, 'Configuration parameter browsers is required.'); + done(); + }); + }); + it('should have an error if config does not have a test_path key', function(done) { + var config = getBaseConfig(); + delete(config.test_path); + browserstackRunner.run(config, function(err) { + assert.equal(err.message, 'Configuration parameter test_path is required.'); + done(); + }); + }); + describe('Check Behaviour with invalid username or key', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(process, 'env', {}); + }); + + it('should have an error if config does not have a username', function(done) { + var config = getBaseConfig(); + delete(config.username); + browserstackRunner.run(config, function(err) { + assert.equal(err.message, 'Configuration parameter username is required.'); + done(); + }); + }); + it('should have an error if config does not have a key', function(done) { + var config = getBaseConfig(); + delete(config.key); + browserstackRunner.run(config, function(err) { + assert.equal(err.message, 'Configuration parameter key is required.'); + done(); + }); + }); + + afterEach(function() { + sandbox.restore(); + }); + }); +}); + +describe('Pass/Fail reporting', function() { + this.timeout(0); + + it('report keys should have browser names', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + var shouldBePresentBrowsers = [ 'Windows 7, Chrome 52.0', 'Windows 7, Firefox 47.0', 'ios 11.2, Iphone ']; + assert.equal(err, null); + reports.forEach(function(report) { + var numMatched = 0; + shouldBePresentBrowsers.forEach(function(browser) { + if(browser === report.browser) { + numMatched++; + } + }); + if(numMatched != 1) { + done(new Error('Report didnt match the shouldBePresentBrowsers for browser: ' + report.browser + ' numMatched: ' + numMatched)); + } else { + var removeIndex = shouldBePresentBrowsers.indexOf(report.browser); + shouldBePresentBrowsers = shouldBePresentBrowsers.slice(0, removeIndex).concat(shouldBePresentBrowsers.slice(removeIndex + 1)); + } + }); + if(shouldBePresentBrowsers.length != 0) { + done(new Error('Browsers not Present in Report: ' + JSON.stringify(shouldBePresentBrowsers))); + } + done(); + }); + }); + it('report keys should have suites and tests', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + assert.notEqual(report.tests, null); + assert.notEqual(report.suites, null); + }); + done(); + }); + }); + describe('Test Tests', function() { + it('report should have proper number of tests', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + assert.equal(report.tests.length, 4); + }); + done(); + }); + }); + it('Each test should have specific keys', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + Object.keys(report.tests).forEach(function(reportKey) { + [ 'name', 'suiteName', 'status', 'runtime', 'errors' ].forEach(function(key) { + assert.notEqual(report.tests[reportKey][key], null); + }); + report.tests[reportKey].assertions.forEach(function(assertion) { + [ 'passed', 'actual', 'expected', 'message' ].forEach(function(key) { + assert.notEqual(assertion[key], null); + }); + }); + }); + }); + done(); + }); + }); + it('Each test should have message in assertions', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + Object.keys(report.tests).forEach(function(reportKey) { + report.tests[reportKey].assertions.forEach(function(assertion) { + assert.notEqual(assertion['message'].match(/(\d+ is .*an .* number|console\..*? exists)/), null); + }); + }); + }); + done(); + }); + }); + }); + describe('Test Suites', function() { + it('report should have Suite of tests', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + assert.notEqual(report.suites, null); + }); + done(); + }); + }); + it('Each Suite should have specific keys', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + [ 'childSuites', 'tests', 'runtime', 'status', 'testCounts' ].forEach(function(key) { + assert.notEqual(report.suites[key], null); + }); + [ 'total', 'passed', 'failed', 'skipped' ].forEach(function(key) { + assert.notEqual(report.suites.testCounts[key], null); + }); + }); + done(); + }); + }); + it('Suites should have correct passed/failed count', function(done) { + var config = getBaseConfig(); + browserstackRunner.run(config, function(err, reports) { + assert.equal(err, null); + reports.forEach(function(report) { + assert.equal(report.suites.testCounts['total'], 4); + assert.equal(report.suites.testCounts['passed'], 2); + assert.equal(report.suites.testCounts['failed'], 2); + assert.equal(report.suites.testCounts['skipped'], 0); + }); + done(); + }); + }); + }); +}); + +describe('Command Line Interface Tests', function() { + this.timeout(0); + it('Should run with valid CLI arguments', function(done) { + execSync('bin/runner.js init'); + exec('bin/runner.js --browsers 1 --path tests/behaviour/resources/qunit_sample.html', null, function(error, stdout, stderr) { + assert.equal(error, null); + done(); + }); + }); + it('Should raise errors if all invalid browser keys.', function(done) { + exec('bin/runner.js --browsers 10 --path tests/behaviour/resources/qunit_sample.html', null, function(error, stdout, stderr) { + assert.notEqual(error.message.match('Invalid'), null); + done(); + }); + }); + it('Should raise error if invalid test path', function(done) { + exec('bin/runner.js --browsers 1 --path invalid/path', function(error, stdout, stderr) { + assert.notEqual(error, null); + assert.notEqual(error.message.match('Invalid'), null); + done(); + }); + }); + it('Should run tests on browsers present if some keys not present', function(done) { + exec('bin/runner.js --browsers 1 10 --path tests/behaviour/resources/qunit_sample.html', null, function(error, stdout, stderr) { + assert.equal(error, null); + done(); + }); + }); + it('Should raise error if empty pid path with pid parameter', function(done) { + exec('bin/runner.js --browsers 1 --path tests/behaviour/resources/qunit_sample.html --pid', null, function(error, stdout, stderr) { + assert.notEqual(error, null); + done(); + }); + }); +}); diff --git a/tests/behaviour/server.js b/tests/behaviour/server.js new file mode 100644 index 0000000..6a68c44 --- /dev/null +++ b/tests/behaviour/server.js @@ -0,0 +1,198 @@ +'use strict'; + +var assert = require('assert'), + sinon = require('sinon'), + path = require('path'), + http = require('http'), + chalk = require('chalk'), + serverPort = 8888, + browserStackRunnerServer = require('../../lib/server.js'); + +var getBaseConfig = function() { + return { + username: 'BROWSERSTACK_USER', + key: 'BROWSERSTACK_KEY', + test_framework: 'qunit', + test_path: path.resolve(__dirname, 'resources', 'qunit_sample.html'), + build: 'BrowserStack Runner Behaviour Tests', + browsers: [ { + browser: 'firefox', + browser_version: '47.0', + os: 'Windows', + os_version: '7' + } ] + } +}; + +var requestServer = function(path, requestBody, appendHeaders, callback) { + var headers = { + 'Content-Length': Buffer.byteLength(requestBody) + } + var request = http.request({ + hostname: 'localhost', + port: serverPort, + path: path, + method: 'POST', + headers: Object.assign(headers, appendHeaders), + }, (res) => { + var responseData = ''; + + res.on('data', (data) => { + responseData += data.toString(); + }); + res.on('end', () => { + callback(null, responseData, res.statusCode); + }); + }).on('error', (e) => { + callback(e); + }); + request.write(requestBody); + request.end(); +}; + +describe('Server Assertions', function() { + describe('Assert logs from the browserstack-runner server', function() { + var sandBox, bsClient, infoLoggerStub, server, reports, workers = {}; + + beforeEach(function() { + sandBox = sinon.sandbox.create(); + bsClient = { + takeScreenshot: sandBox.stub() + }, + infoLoggerStub = sandBox.stub(browserStackRunnerServer.logger, 'info'); + + server = browserStackRunnerServer.Server(bsClient, workers, getBaseConfig(), function(error, reports) { + console.log('Dude!', reports); + }); + server.listen(serverPort); + }); + + afterEach(function() { + sandBox.restore(); + server.close(); + }); + + it('logs console.log correctly', function(done) { + var browserString = 'OS X Chrome 54' + requestServer('/_log', '{"arguments":["Random String"]}', { + 'x-browser-string': browserString + }, function(error) { + if(error) done(error); + assert.equal(infoLoggerStub.called, true); + assert.equal(infoLoggerStub.callCount, 1); + assert.equal(infoLoggerStub.getCalls()[0].args, '[' + browserString + '] ' + 'Random String'); + + requestServer('/_log', '{"arguments":["Invalid Random String', { + 'x-browser-string': browserString + }, function(error) { + if(error) done(error); + assert.equal(infoLoggerStub.callCount, 2); + assert.equal(infoLoggerStub.getCalls()[1].args, '[' + browserString + '] ' + '{"arguments":["Invalid Random String'); + done(); + }); + }); + }); + + it('logs test errors correctly', function(done) { + var browserUUIDString = 'abcd-efgh-1234-5678', + browserInfoString = 'browserInfo'; + + workers[browserUUIDString] = { + getTestBrowserInfo: sandBox.stub().returns(browserInfoString), + string: 'workerString' + }; + var requestBodyObject = { + test: { + errors: [{ + message: "failedTestMessage", + actual: "ActualValue", + expected: "expectedValue", + source: "LongStackTrace" + }], + name:"customTestName", + suiteName:"customSuiteName" + } + }; + + requestServer('/_progress', JSON.stringify(requestBodyObject), { + 'x-worker-uuid': browserUUIDString + }, function(error) { + if(error) done(error); + assert.equal(infoLoggerStub.called, true); + assert.equal(infoLoggerStub.callCount, 1); + assert.equal(infoLoggerStub.getCalls()[0].args.length, 3); + assert.equal(infoLoggerStub.getCalls()[0].args[0], '[%s] ' + chalk.red('Error:')); + assert.equal(infoLoggerStub.getCalls()[0].args[1], browserInfoString); + assert.equal(infoLoggerStub.getCalls()[0].args[2], + '"customTestName" failed, failedTestMessage\n' + chalk.blue('Expected: ') + 'expectedValue' + + '\n' + chalk.blue(' Actual: ') + 'ActualValue' + + '\n' + chalk.blue(' Source: ') + 'LongStackTrace' + ); + + requestServer('/_progress', '{"arguments":["Invalid Random String', { + 'x-worker-uuid': browserUUIDString + }, function(error) { + if(error) done(error); + assert.equal(infoLoggerStub.callCount, 3); + assert.equal(infoLoggerStub.getCalls()[1].args.length, 2); + assert.equal(infoLoggerStub.getCalls()[1].args[0], '[%s] Exception in parsing log'); + assert.equal(infoLoggerStub.getCalls()[1].args[1], 'workerString'); + + assert.equal(infoLoggerStub.getCalls()[2].args.length, 2); + assert.equal(infoLoggerStub.getCalls()[2].args[0], '[%s] Log: undefined'); + assert.equal(infoLoggerStub.getCalls()[2].args[1], 'workerString'); + done(); + }); + }); + }); + + it('logs for test reports correctly', function(done) { + var browserUUIDString = 'abcd-efgh-1234-5678', + browserString = 'OS X Chrome 41', + browserInfoString = 'browserInfo'; + + workers[browserUUIDString] = { + getTestBrowserInfo: sandBox.stub().returns(browserInfoString), + string: 'workerString' + }; + var requestBodyObject = { + testCounts: { + total: 1, + passed: 1, + failed: 0, + skipped: 0 + }, + runtime: '00:01:00', + status: 'passed' + }; + + requestServer('/_report', JSON.stringify(requestBodyObject), { + 'x-worker-uuid': browserUUIDString + }, function(error) { + if(error) done(error); + assert.equal(infoLoggerStub.called, true); + assert.equal(infoLoggerStub.callCount, 1); + assert.equal(infoLoggerStub.getCalls()[0].args.length, 7); + assert.equal(infoLoggerStub.getCalls()[0].args[0], '[%s] ' + chalk['green']('Passed:') + ' %d tests, %d passed, %d failed, %d skipped; ran for %dms'); + assert.equal(infoLoggerStub.getCalls()[0].args[1], browserInfoString); + assert.equal(infoLoggerStub.getCalls()[0].args[2], 1); + assert.equal(infoLoggerStub.getCalls()[0].args[3], 1); + assert.equal(infoLoggerStub.getCalls()[0].args[4], 0); + assert.equal(infoLoggerStub.getCalls()[0].args[5], 0); + assert.equal(infoLoggerStub.getCalls()[0].args[6], '00:01:00'); + + requestServer('/_report', '{"arguments":["Invalid Random String', { + 'x-worker-uuid': browserUUIDString, + 'x-browser-string': browserString + }, function(error) { + if(error) done(error); + assert.equal(infoLoggerStub.callCount, 2); + assert.equal(infoLoggerStub.getCalls()[1].args.length, 2); + assert.equal(infoLoggerStub.getCalls()[1].args[0], '[%s] Null response from remote Browser'); + assert.equal(infoLoggerStub.getCalls()[1].args[1], browserString); + done(); + }); + }); + }); + }); +}); diff --git a/tests/external-tests.js b/tests/external-tests.js new file mode 100755 index 0000000..590ed30 --- /dev/null +++ b/tests/external-tests.js @@ -0,0 +1,205 @@ +#! /usr/bin/env node + +var path = require('path'); +var Helper = require('./helper'); + +var browserstackConfig = { + username: 'BROWSERSTACK_USERNAME', + key: 'BROWSERSTACK_KEY' +}; + +var mode = (process.env.TEST_MODE || 'all').toLowerCase(); +var runnerPath = path.resolve(path.join(__dirname, '..', 'bin', 'runner.js')); +var testHome = path.resolve(__dirname); +process.chdir(testHome); + +/** + * Mocha v2.4.5 - to change with another Mocha version or + * something with Mocha tests + * + * index.html - 22 tests, 18 passed, 4 failed -> one test is displayed twice, + * so they are displayed 5 failing tests, but counted only 4 + * large.html - 64 tests, 60 passed, 4 failed -> only 2 tests are failing, but + * they are displayed twice + * opts.html - 8 tests, 2 passed, 6 failed -> only 3 tests are failing, but + * they are displayed twice + * + * By "displayed" it is referred the Mocha HTML Reporter. + * + * From the above explanations it is clear that there are some inconsistencies, + * also because Mocha's HTML Reporter counted number of tests does not match + * the number of displyed tests. + * + * The cause is (snippet from Mocha's HTML reporter): + * + * runner.on('fail', function(test) { + * // For type = 'test' its possible that the test failed due to multiple + * // done() calls. So report the issue here. + * if (test.type === 'hook' + * || test.type === 'test') { + * runner.emit('test end', test); + * } + * }); + * + * This is why failed tests are displayed twice... + * + * The JsReporters is counting the tests on the "test end" event, that's why + * it is capturing the failing tests twice, in the "index.html" it does not + * capture everything, because there is an async test, which failure is + * triggered after a timeout and the JsReporters is not waiting, because + * it cannot know how much to wait. + * + * + * This been said, the JsReporter MochaAdapter is functioning well, this + * version of Mocha is not reliable and should be changed. + */ + +var repositories = [ + { + name: 'qunit', + tag: '1.21.0', + url: 'https://github.com/jquery/qunit.git', + test_framework: 'qunit', + browsers: [ + { + 'browser': 'firefox', + 'browser_version': '44.0', + 'os': 'OS X', + 'os_version': 'El Capitan' + } + ], + test_path: [ + 'test/index.html' + ], + expected_results: { + tests: 133, + passed: 130, + failed: 0 + } + }, + { + name: 'mocha', + tag: 'v2.4.5', + url: 'https://github.com/mochajs/mocha.git', + test_framework: 'mocha', + browsers: [ + { + 'browser': 'ie', + 'browser_version': '11.0', + 'os': 'Windows', + 'os_version': '10' + } + ], + test_path: [ + 'test/browser/index.html', + 'test/browser/large.html', + 'test/browser/opts.html' + ], + expected_results: { + tests: 86, + passed: 78, + failed: 8 + } + }, + { + name: 'spine', + tag: 'v.1.6.2', + url: 'https://github.com/spine/spine.git', + test_framework: 'jasmine2', + browsers: [ + { + 'browser': 'safari', + 'browser_version': '9.0', + 'os': 'OS X', + 'os_version': 'El Capitan' + } + ], + test_path: [ + 'test/index.html' + ], + expected_results: { + tests: 161, + passed: 161, + failed: 0 + } + }, + { + name: 'spine', + tag: 'v1.0.0', + url: 'https://github.com/spine/spine.git', + test_framework: 'jasmine', + browsers: [ + { + 'browser': 'safari', + 'browser_version': '9.0', + 'os': 'OS X', + 'os_version': 'El Capitan' + } + ], + test_path: [ + 'test/index.html' + ], + patches: [ + { + find: 'jasmine.getEnv().execute();', + replace: 'window.onload = function () { jasmine.getEnv().execute(); };' + } + ], + expected_results: { + tests: 63, + passed: 63, + failed: 0 + } + } +]; + +var repositoriesOptional = [ + { + name: 'mocha', + tag: '1.21.5', + url: 'https://github.com/mochajs/mocha.git', + test_framework: 'mocha', + browsers: [ + { + 'browser': 'ie', + 'browser_version': '10.0', + 'os': 'Windows', + 'os_version': '7' + } + ], + test_path: [ + 'test/browser/index.html', + 'test/browser/large.html', + 'test/browser/opts.html' + ], + expected_results: { + tests: 83, + passed: 77, + failed: 6 + } + } +]; + +function run(repositories) { + Helper.runRepositories(browserstackConfig, repositories, testHome, runnerPath, function (err) { + if (err) { + console.log(err.stack); + throw err; + } + + console.log('Done.'); + }); +} + +switch (mode) { + case 'required': + run(repositories); + break; + + case 'optional': + run(repositoriesOptional); + break; + + default: + run([].concat(repositories).concat(repositoriesOptional)); +} diff --git a/tests/helper.js b/tests/helper.js new file mode 100644 index 0000000..feac305 --- /dev/null +++ b/tests/helper.js @@ -0,0 +1,259 @@ + +var exec = require('child_process').execFile; +var execSync = require('child_process').execFileSync; +var fs = require('fs'); +var path = require('path'); +var util = require('util'); + +module.exports = { + runRepositories: runRepositories, + runRepository: runRepository +}; + +function runRepositories(browserstackConfig, repositories, testHome, runnerPath, callback) { + var repository = repositories.shift(); + if (!repository) { + return callback(null); + } + + runRepository(testHome, runnerPath, repository, browserstackConfig, function (err) { + if (err) { + return callback(err); + } + + process.nextTick(function () { + runRepositories(browserstackConfig, repositories, testHome, runnerPath, callback); + }); + }); +} + + +function runRepository(testHome, runnerPath, repository, config, callback) { + var done = function () { + try { + process.chdir(testHome); + } catch(e) { + return callback('Error switching to test directory: ' + e); + } + + callback.apply(null, Array.prototype.slice.call(arguments, 0)); + }; + + try { + fs.mkdirSync(repository.test_framework); + } catch (e) { + // ignore + } + + process.chdir(repository.test_framework); + repository.branch = repository.branch || repository.tag; + + var dirName = repository.name + '-' + repository.branch; + var conf = {}; + for (var k in config) { + conf[k] = config[k]; + } + + gitCloneByBranch(repository, dirName, function (err) { + if (err && !err.message.match(/already exists/)) { + return done(err); + } + + try { + console.log('Switching to repository:', dirName); + process.chdir(dirName); + } catch (e) { + return callback('Error switching to project directory: ' + e); + } + + if (repository.patches && repository.patches.length) { + patchFiles(repository.test_path, repository.patches); + } + + conf.test_framework = repository.test_framework; + conf.browsers = repository.browsers; + conf.project = repository.name; + + var ciPrefix = process.env.TRAVIS_BUILD_NUMBER; + conf.build = (ciPrefix ? ciPrefix + '-' : '') + repository.branch; + conf.test_path = repository.test_path; + + runTests(runnerPath, process.cwd(), conf, repository.expected_results, done); + }); +} + +function gitCloneByBranch(repository, dirName, callback) { + fs.lstat(dirName, function (err, stat) { + var dirExistsError = new Error(dirName + ' already exists'); + if (err && (err.code !== 'ENOENT' || err.errno !== -2)) { + return callback(err); + } + + if (stat && stat.isDirectory()) { + return callback(dirExistsError); + } + + var cmd = util.format('git clone -b %s --single-branch --depth 1 %s %s', repository.branch, repository.url, dirName); + console.log('Executing:', cmd); + var cmdParts = cmd.split(' '); + + runCommand(cmdParts.shift(), cmdParts, false, null, callback); + }); +} + +function patchFiles(files, patches) { + if (files && files.length && patches && patches.length) { + files.forEach(function (f) { + try { + var content = fs.readFileSync(f, 'utf8'); + patches.forEach(function (p) { + if (content.indexOf(p.replace) === -1) { + content = content.replace(p.find, p.replace); + } + }); + + fs.writeFileSync(f, content, 'utf8'); + } catch (e) { + console.warn(e); + } + }); + } +} + +function initRepository() { + try { + execSync('npm', [ 'install' ]); + } catch (e) { + console.error(e.message || e.toString()); + } + + try { + var stat = fs.lstatSync('bower.json'); + if (stat && stat.isFile()) { + execSync('bower', [ 'install' ]); + } + } catch (e) { + if (e.code !== 'ENOENT' || e.errno !== -2) { + console.warn(e.message || e.toString()); + } + } +} + +function runTests(runnerPath, projectDir, conf, expectedResults, callback) { + var results = { + tests: 0, + passed: 0, + failed: 0 + }; + + initRepository(); + + var confPath = path.join(process.cwd(), 'browserstack.json'); + var confString = JSON.stringify(conf, null, 4); + console.log('Creating config (%s):\n%s', confPath, confString); + + fs.writeFile(confPath, confString, 'utf8', function (err) { + if (err) { + return callback(err); + } + + console.log('Running tests:', projectDir); + runCommand(runnerPath, [], true, function (data, done) { + if (data && data.length) { + var matches = data.match(/\[(.*)\] (passed|failed): (\d+) tests, (\d+) passed, (\d+) failed.*[^\n]/i); + if (matches && matches.length > 5) { + // results.pages.push(matches[1].split(', ').slice(2).join('')); + + [ 'failed', 'passed', 'tests' ].forEach(function (k) { + results[k] += parseInt(matches.pop()); + }); + + console.log('>', data.trim()); + } + } + + // continue until end + done(false); + }, function (err) { + if (err) { + return callback(err); + } + + var diff = Object.keys(results).reduce(function (o, k) { + if (isFinite(expectedResults[k]) && expectedResults[k] !== results[k]) { + o.push(util.format('Mismatch in %s: %d !== %d', k, results[k], expectedResults[k])); + } + + return o; + }, []); + + callback(diff.length ? new Error(diff.join('\r\n')) : null, results); + }); + }); +} + +function runCommand(cmd, args, ignoreErr, processOutputHook, callback) { + var isRunning = true, + output = '', + subProcess, + timeoutHandle; + + if (!processOutputHook) { + processOutputHook = function (data, done) { + output += data; + done(); + }; + } + + var callbackOnce = function (err, result) { + clearTimeout(timeoutHandle); + if (subProcess && isRunning) { + try { + process.kill(subProcess.pid, 'SIGKILL'); + subProcess = null; + } catch (e) { + } + } + + callback && callback(err, result); + callback = null; + }; + + var processOutput = function (isError) { + return function (data) { + processOutputHook(data, function (isDone) { + if (isDone) { + isError ? callbackOnce(new Error(data)) : callbackOnce(null, data); + } + }); + }; + }; + + try { + subProcess = exec(cmd, args, function (error, stdout, stderr) { + isRunning = false; + + if (error) { + if (ignoreErr) { + if (stdout && !stdout.match(/tests done, failures/)) { + console.warn(stdout || stderr); + console.log(error.stack); + } + + callbackOnce(null); + } else { + callbackOnce(new Error('failed to get process output: ' + error)); + } + } else { + callbackOnce(null, stdout || stderr || output || error); + } + }); + + subProcess.stdout.on('data', processOutput(false)); + subProcess.stderr.on('data', processOutput(true)); + } catch (e) { + // Handles EACCESS and other errors when binary file exists, + // but doesn't have necessary permissions (among other issues) + callbackOnce(new Error('failed to get process output: ' + e)); + } +} diff --git a/tests/test.rb b/tests/test.rb index f24a491..c3cfcc3 100644 --- a/tests/test.rb +++ b/tests/test.rb @@ -12,7 +12,7 @@ def runBg(cmd, linestomatch, genre, test_strings) matched_counter = 0 IO.popen(cmd) {|io| io.each {|line| - if line.match(/Completed in /) + if line.match(/ran for/) counter += 1 puts "[#{genre.upcase}] : " + line test_strings.each do |test_string| @@ -45,27 +45,27 @@ def run_project(reponame, test_framework, passed_array, browser_url) sleep(1) execute("cd #{repo_path}#{reponame} && git clean -f -d && git reset --hard") end - + def qunit - run_project("underscorejs", "qunit", ["631 of 631 passed"], "test/index.html") - run_project("Modernizr", "qunit", ["20 of 41 passed"], "test/index.html") + run_project("underscorejs", "qunit", ["631 passed"], "test/index.html") + run_project("Modernizr", "qunit", ["20 passed"], "test/index.html") # https://github.com/bitovi/funcunit/ # https://github.com/twbs/bootstrap end def mocha - run_project("url.js", "mocha", ["35 of 35 passed"], "test.html") + run_project("url.js", "mocha", ["35 passed"], "test.html") # https://github.com/browserstack/microjungle # https://github.com/dhimil/mocha # https://github.com/auth0/auth0.js end def jasmine - run_project("mout", "jasmine", ["978 of 978 passed"], "tests/runner.html") + run_project("mout", "jasmine", ["978 passed"], "tests/runner.html") end def jasmine2 - run_project("Comparatorsjs", "jasmine2", ["6 of 6 passed"], "comparators.spec-runner.html") + run_project("Comparatorsjs", "jasmine2", ["6 passed"], "comparators.spec-runner.html") end def run diff --git a/tests/unit/utils_spec.js b/tests/unit/utils_spec.js index 73e6eee..62b75b9 100644 --- a/tests/unit/utils_spec.js +++ b/tests/unit/utils_spec.js @@ -6,7 +6,7 @@ describe('Utilities', function(){ assert.equal("Hello", utils.titleCase("hello")); assert.equal("Are You Serious?", utils.titleCase("are you serious?")); }); - + it('should generate 32 char uuid', function(){ assert.equal(5, utils.uuid().split("-").length); assert.equal(32 + 4, utils.uuid().length); @@ -16,7 +16,7 @@ describe('Utilities', function(){ assert.notEqual(utils.uuid(), utils.uuid()); assert.notEqual(utils.uuid(), utils.uuid()); }); - + it('should generate proper browser string for config', function(){ var chrome_mac = {os: "OS X", os_version: "Mavericks", "browser": "chrome", "browser_version": "latest"}; var chrome_windows = {os: "Windows", os_version: "XP", "browser": "chrome", "browser_version": "latest"}; @@ -24,16 +24,28 @@ describe('Utilities', function(){ var ie_windows = {os: "Windows", os_version: "7", "browser": "ie", "browser_version": "9.0"}; var iOS = {os: "iOS", os_version: "6.0", device: "iPhone 5"}; var androidConfig = {os: "android", os_version: "4.1"}; - + assert.equal("OS X Mavericks, Chrome latest", utils.browserString(chrome_mac)); assert.equal("Windows XP, Chrome latest", utils.browserString(chrome_windows)); assert.equal("Windows 7, Internet Explorer 9.0", utils.browserString(ie_windows)); assert.equal("iOS 6.0, iPhone 5", utils.browserString(iOS)); assert.equal("android 4.1", utils.browserString(androidConfig)); }); - + it('should return number of keys for this object', function(){ assert.equal(0, utils.objectSize({})); assert.equal(1, utils.objectSize({a: 2})); }); + + it('should escape special characters incompatible with JSON.parse', function () { + var testString = '{"tracebacks":[{"actual":null,"message":"Died on test #1 at http://localhost:8888/test/main/globals.js:43:7\n at http://localhost:8888/test/main/globals.js:67:2: Error","testName":"globals: Exported assertions"}]}'; + var expectString = '{"tracebacks":[{"actual":null,"message":"Died on test #1 at http://localhost:8888/test/main/globals.js:43:7\\n at http://localhost:8888/test/main/globals.js:67:2: Error","testName":"globals: Exported assertions"}]}'; + + var malformedJson = '{ "key" : "Bad\njson contains\rall\tsorts\bof\vhorrible\0 & nasty\fescape sequences" }'; + var expectJson = { "key" : "Bad\njson contains\rall\tsorts\bof\u000bhorrible\u0000 & nasty\fescape sequences" }; + + assert.throws(function () { JSON.parse(testString); }, SyntaxError, 'JSON.parse fails'); + assert.equal(testString.escapeSpecialChars(), expectString); + assert.equal(JSON.parse(malformedJson.escapeSpecialChars()).key, expectJson.key); + }); }); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..18df6ec --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,17 @@ +var webpack = require("webpack"); + +module.exports = { + entry: "./lib/client-browserstack-util.js", + output: { + path: "./lib/_patch", + filename: "browserstack-util.js" + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + minimize: true, + comments: false, + compress: { warnings: false }, + sourceMap: false + }) + ] +};