├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .npmrc ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE.md ├── MAINTAINERS.md ├── README.md ├── TODO.md ├── bin ├── chimp.js ├── chimpy.js ├── findBrokenFeatures.js └── findBrokenFeatures │ ├── findBrokenFeatures.js │ ├── findBrokenFeatures.test.js │ └── sample-for-test.json ├── chimp.js ├── circle.yml ├── docs ├── README.md ├── _config.yml ├── cheat-sheet.md ├── chimp-meteor.md ├── cloud-services.md ├── configuration.md ├── credits.md ├── debugging.md ├── further-reading.md ├── getting-started-cucumber.md ├── getting-started-jasmine.md ├── getting-started-mocha.md ├── images │ ├── ci.png │ ├── header.png │ ├── meteor.png │ ├── realtime.gif │ ├── test-frameworks.png │ └── wdio.png ├── installation.md ├── jasmine-support.md ├── migrate-to-synchronous.md ├── multi-browser-testing.md ├── reporting.md └── synchronous-tests.md ├── features ├── es2015.feature ├── step_definitions │ ├── es2015-steps.js │ └── webdriver-steps.js └── webdriver.feature ├── index.js ├── mocha-setup.js ├── mocha.opts ├── package-lock.json ├── package.json ├── scripts ├── lib │ └── exec.js ├── release-notes.ejs ├── release.sh ├── run-tests.js └── run.js ├── src ├── __mocks__ │ ├── .babelignore │ ├── .babelrc │ ├── .eslintrc │ ├── bluebird.js │ ├── chokidar.js │ ├── colors.js │ ├── ddp.js │ ├── freeport.js │ ├── hapi.js │ ├── request.js │ ├── selenium-standalone.js │ ├── webdriverio.js │ └── xolvio-sync-webdriverio.js ├── __tests__ │ ├── .babelignore │ ├── .babelrc │ ├── .eslintrc │ ├── browserstack-manager-spec.js │ ├── chimp-spec.js │ ├── ddp-spec.js │ ├── options-loader.js │ ├── phantom-spec.js │ ├── process-helper-spec.js │ ├── saucelabs-manager-spec.js │ ├── selenium-spec.js │ ├── session-factory-spec.js │ ├── session-manager-spec.js │ ├── simian-reporter-spec.js │ └── testingbot-manager-spec.js ├── bin │ ├── chimp.js │ └── default.js └── lib │ ├── babel-register.js │ ├── boolean-helper.js │ ├── browserstack-manager.js │ ├── chimp-helper.js │ ├── chimp-spec.js │ ├── chimp.js │ ├── chromedriver.js │ ├── ci.js │ ├── consoler.js │ ├── cucumberjs │ ├── cucumber-wrapper.js │ ├── cucumber.js │ ├── hooks.js │ └── world.js │ ├── ddp-watcher.js │ ├── ddp.js │ ├── environment-variable-parsers.js │ ├── jasmine │ ├── jasmine-fiberized-api.js │ ├── jasmine-helpers.js │ ├── jasmine-wrapper.js │ └── jasmine.js │ ├── log.js │ ├── mocha │ ├── mocha-fiberized-ui.js │ ├── mocha-helper.js │ ├── mocha-wrapper-instance.js │ ├── mocha-wrapper-spec.js │ ├── mocha-wrapper.js │ └── mocha.js │ ├── options-loader.js │ ├── phantom.js │ ├── process-helper.js │ ├── saucelabs-manager.js │ ├── screenshot-helper.js │ ├── selenium.js │ ├── session-factory.js │ ├── session-manager.js │ ├── simian-reporter.js │ ├── testingbot-manager.js │ ├── utils │ ├── escape-reg-exp.js │ └── fiberize.js │ └── versions.js ├── tests ├── .babelignore ├── .babelrc ├── .eslintrc ├── cucumber │ └── features │ │ ├── globalPending.feature │ │ └── step_definitions │ │ └── Given_global_pending_is_defined.js ├── jasmine │ └── jasmine-spec.js └── mocha │ └── mocha-spec.js ├── wallaby-mocha.js └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-export-default-from", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties"], 3 | "presets": ["@babel/preset-env"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "jasmine": true 7 | }, 8 | "globals" : { 9 | "jest": true, 10 | "browser": true 11 | }, 12 | "rules": { 13 | "object-curly-spacing": 0, 14 | "no-use-before-define": [2, "nofunc"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 34 | ##### Expected behaviour 35 | 36 | 37 | ##### Actual behaviour 38 | 39 | 40 | ##### Exact steps to reproduce / repository that demonstrates the problem 41 | Please put long code snips in a [Gist](https://gist.github.com/) and provide a link here. 42 | 43 | ##### Version & tools: 44 | 45 | * Chimp command line used: *The exact command you used* 46 | * Chimp config file: *Please provide a Gist if you use a config file* 47 | * Chimp: *type `chimp -v`* 48 | * Node.js: *type `node -v`* 49 | * Java: *type `java -version`* 50 | * Operation system: *type `uname -v`* 51 | 52 | ##### Console / Log Output 53 | 54 | Please put long logs in a [Gist](https://gist.github.com/) and provide a link here. 55 | 56 | 57 | ------- 58 | *Join our Slack [xolv.io/community](http://xolv.io/community) #chimp channel, where you can find help and help others.* 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | node_modules 9 | tmp 10 | dist 11 | output.json 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | src 9 | scripts 10 | tmp 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Reporting a bug? 4 | 5 | Thank you for taking the time to come here and report a bug! In order to get you the best help and 6 | so that you get a quick response, please consider this checklist for reporting a bug: 7 | 8 | * [ ] Provide an explanation of what you're expecting and what is actually happening 9 | * [ ] Attach the code that is causing the issue 10 | * [ ] Attach console outputs 11 | * [ ] Run chimp with the `--debug` flag 12 | 13 | Please be sure to include any logs inside a fenced code block 14 | [like this](https://help.github.com/articles/github-flavored-markdown/#fenced-code-blocks). This 15 | makes it easier to read. 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Xolv.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # For NPM Chimp 2 | 3 | ## ES2015 4 | 5 | * We use Babel to compile our ES2015 code to ES5. 6 | * The compiled ES5 code is in the `dist/` folder. 7 | * You can compile the code with `npm run prepare`. 8 | This is done automatically before you publish a release. 9 | 10 | ## Watch 11 | 12 | You can run `npm run watch` to use [npm-watch](https://www.npmjs.com/package/npm-watch) to 13 | run the npm task `prepare` automatically on change to `src/lib` or `src/bin`. 14 | 15 | ## Running tests 16 | 17 | You can use [Wallaby](http://wallabyjs.com/) if you own it or just run `npm test`. 18 | 19 | ## Running Chimp 20 | 21 | The run script will compile the source code before running Chimp. 22 | 23 | ```sh 24 | ./scripts/run.js 25 | ``` 26 | 27 | ## Release a new version 28 | 29 | * Update the documentation 30 | * Bump the version with `npm version `. 31 | Use the [Semantic versioning](http://semver.org/) conventions for bumping the version. 32 | * Publish the new release with: `npm publish` 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package requires chromedriver to be installed as a dependency in your project. 2 | 3 | To install both packages: 4 | 5 | ``` 6 | npm i --save-dev chimpy chromedriver 7 | ``` 8 | 9 | # IMPORTANT ANNOUNCEMENT - July 22nd, 2018 10 | 11 | The Chimp that you know and love is now being split into two separate projects, both of which are intended to help you deliver higher quality faster. 12 | 13 | The first is [*Chimpy*](https://github.com/TheBrainFamily/chimpy), which will be developed and maintained by [TheBrain team](http://team.thebrain.pro). This project will continue evolving and supporting the current thinking behind Chimp 1.x. 14 | 15 | The second is [*Chimp 2.0*](https://github.com/xolvio/chimp), which will be developed and maintained by [Xolv.io](http://xolv.io). This project will be built from scratch based on the learnings made while using Chimp 1.x in the field. 16 | 17 | For more details about this decision, [please see the full announcement here.](https://medium.com/@sam_hatoum/the-future-of-chimp-js-e911f8e9aaa6) 18 | 19 | --- 20 | 21 | Access the [documentation site here.](https://thebrainfamily.github.io/chimpy) 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Chimp > Chimpy Migration TODO list 2 | 3 | Lukasz 4 | * [x] Publish the chimpy npm package 5 | * [x] Create a chimpy bin 6 | * [x] keep chimp bin but make it tell people to use chimpy instead 7 | * [x] Create CircleCI jobs 8 | * [ ] Create a page on TheBrain that tells people they can request help for $$ - the Chimpy experts 9 | * [x] migrate internal project to chimpy for testing 10 | * [x] Add the retry feature 11 | 12 | Sam 13 | * [ ] configure docs using GHPages 14 | * [ ] rename chimp channel to chimpy 15 | * [ ] Link the blog post 16 | - include the retry feature in the announcement 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /bin/chimp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const log = require('../dist/lib/log.js'); 3 | const colors = require('colors'); 4 | colors.enabled = true; 5 | log.info("\n\nThanks for using Chimpy!".green); 6 | 7 | log.info("\nPlease change your chimp calls to chimpy.".yellow, "\nUsing the old name - chimp - to run chimpy is deprecated.\nIn the near future it will collide with chimp 2.0 project.\n\n".yellow ); 8 | 9 | setTimeout(() => { 10 | require('../dist/bin/chimp'); 11 | }, 5000); -------------------------------------------------------------------------------- /bin/chimpy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@babel/register') 4 | 5 | // be sure to run `npm run prepare` first to generate the dist dir 6 | require('../dist/bin/chimp'); 7 | -------------------------------------------------------------------------------- /bin/findBrokenFeatures.js: -------------------------------------------------------------------------------- 1 | const { findBrokenFeatures } = require('./findBrokenFeatures/findBrokenFeatures') 2 | 3 | const jsonfile = require('jsonfile') 4 | const jsonOutput = jsonfile.readFileSync(process.argv[2]) 5 | console.log(findBrokenFeatures(jsonOutput).join(" ")) 6 | -------------------------------------------------------------------------------- /bin/findBrokenFeatures/findBrokenFeatures.js: -------------------------------------------------------------------------------- 1 | const { uniq } = require('underscore') 2 | const findBrokenFeatures = (testResults) => { 3 | const foundBroken = []; 4 | testResults.forEach(feature => { 5 | feature.elements.forEach(element => { 6 | element.steps.forEach(step => { 7 | if (step.result.status !== "passed") { 8 | foundBroken.push(feature.uri) 9 | } 10 | }) 11 | }) 12 | }) 13 | return uniq(foundBroken) 14 | } 15 | 16 | module.exports = { 17 | findBrokenFeatures 18 | } -------------------------------------------------------------------------------- /bin/findBrokenFeatures/findBrokenFeatures.test.js: -------------------------------------------------------------------------------- 1 | const { findBrokenFeatures } = require('./findBrokenFeatures') 2 | const oneBroken = require('./sample-for-test.json') 3 | 4 | test('works', () => { 5 | const listOfFailingFeatures = findBrokenFeatures(oneBroken) 6 | expect(listOfFailingFeatures).toEqual( ["/Users/user/projects/prototypes/chimpRerun/features/test.feature"]) 7 | }) 8 | -------------------------------------------------------------------------------- /chimp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // - - - - CHIMP - - - - 3 | watch: false, 4 | watchTags: '@watch', 5 | offline: false, 6 | 7 | // - - - - CUCUMBER - - - - 8 | path: './features', 9 | 10 | jsonOutput: 'output.json', 11 | 12 | // '- - - - DEBUGGING - - - - 13 | log: 'info', 14 | debug: false, 15 | seleniumDebug: false, 16 | webdriverLogLevel: false, 17 | // debugBrkCucumber: 5858, 18 | }; 19 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | node: 5 | version: 6.11.1 6 | environemnt: 7 | DBUS_SESSION_BUS_ADDRESS: /dev/null 8 | 9 | general: 10 | branches: 11 | ignore: 12 | - gh-pages 13 | 14 | dependencies: 15 | cache_directories: 16 | - "node_modules" 17 | - "~/.npm" 18 | 19 | pre: 20 | - google-chrome --version 21 | - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 22 | - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' 23 | - sudo apt-get update 24 | - sudo apt-get --only-upgrade install google-chrome-stable 25 | - google-chrome --version 26 | - sudo apt-get update 27 | - sudo apt-get install libpango1.0-0 28 | - sudo apt-get install firefox 29 | - sudo ln -sf /usr/lib/firefox/firefox /usr/bin/firefox 30 | override: 31 | - npm install 32 | 33 | test: 34 | override: 35 | - npm run testonly: 36 | parallel: true 37 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | 3 | title: Chimpy.js 4 | 5 | description: An awesome developer-centric experience to writing tests with realtime feedback using Mocha, Jasmine or Cucumber.js 6 | 7 | show_downloads: false 8 | 9 | plugins: 10 | - jekyll-sitemap 11 | -------------------------------------------------------------------------------- /docs/cheat-sheet.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | The following is a list of examples of how to do common tasks and assertions using: 4 | * Webdriver.io 5 | * Synchronous execution 6 | * Jasmine expectations 7 | 8 | *[Click here the full WebdriverIO API](http://v4.webdriver.io/api.html)* 9 | 10 | The variable `browser` is a handle to webdriver. It is shorthand for, or the equivalent of: 11 | 12 | `var webdriverio = require('webdriverio'); 13 | var options = {}; 14 | var browser=webdriverio.remote(options)` 15 | 16 | ###### Assert that element exists in DOM 17 | ```javascript 18 | const doesExist = client.waitForExist("selector"); 19 | 20 | expect(doesExist).toBe(true); 21 | ``` 22 | 23 | ###### Assert that element is not in DOM (removed) 24 | Make sure to wait for the parent element to exist. The parent and checked element should be rendered at the same time for the check to not give a false positive. 25 | 26 | Use `null` for second parameter to keep default wait timeout. Third parameter negates the check. 27 | 28 | ```javascript 29 | client.waitForExist("parentSelector"); 30 | const doesNotExist = client.waitForExist("childElement", null, true); 31 | 32 | expect(doesNotExist).toBe(true); 33 | ``` 34 | 35 | ###### Assert on number of elements in DOM 36 | ```javascript 37 | client.waitForExist("selector"); 38 | const elements = client.elements("selector"); 39 | 40 | expect(elements.value.length).toEqual(2); 41 | // expect(elements.value.length).toBeGreaterThan(2); 42 | // expect(elements.value.length >= 2).toBe(true); 43 | ``` 44 | 45 | ###### Assert that a single element has text value (assuming only 1 match exists for the selector) 46 | ```javascript 47 | client.waitForExist("selector"); 48 | client.moveToObject("selector"); 49 | const actualText = client.getText("selector"); 50 | 51 | expect(actualText).toEqual("text"); 52 | ``` 53 | 54 | ###### Assert that a single element in a list of matches for a selector has text value 55 | ```javascript 56 | client.waitForExist("selector"); 57 | const elements = client.elements("selector"); 58 | const element = elements.value[]; 59 | client.moveTo(element.ELEMENT); 60 | const actualText = client.elementIdText(element.ELEMENT).value; 61 | expect(actualText).toEqual("text"); 62 | ``` 63 | 64 | ###### Find a single element in a list of matches based on its text value 65 | ```javascript 66 | const elements = client.elements("selector"); 67 | let matchingElementId = null; 68 | for (const element of elements.value) { 69 | const text = client.elementIdText(element.ELEMENT).value; 70 | if (text === textToMatch) { 71 | matchingElementId = element.ELEMENT; 72 | break; 73 | } 74 | } 75 | expect(matchingElementId).not.toBe(null); 76 | client.moveTo(matchingElementId); 77 | ``` 78 | 79 | ###### Assert that element has class 80 | ```javascript 81 | client.waitForExist("selector"); 82 | const cssClass = client.getAttribute("selector", "class"); 83 | 84 | expect(cssClass).toContain("my-custom-class"); 85 | ``` 86 | 87 | ###### Click a single element (assuming only 1 match exists for the selector) 88 | ```javascript 89 | client.waitForExist("selector"); 90 | client.moveToObject("selector"); 91 | client.click("selector"); 92 | ``` 93 | 94 | ###### Click a single element in a list of matches for a selector 95 | ```javascript 96 | client.waitForExist("selector"); 97 | const elements = client.elements("selector"); 98 | const element = elements.value[]; 99 | client.moveTo(element.ELEMENT); 100 | client.elementIdClick(element.ELEMENT); 101 | ``` 102 | 103 | ###### Move to an element 104 | ```javascript 105 | client.waitForExist("selector"); 106 | client.moveToObject("selector"); 107 | ``` 108 | 109 | ###### Drag an element 110 | ```javascript 111 | client.waitForExist("selector"); 112 | client.moveToObject("selector", 0, 0); 113 | client.buttonDown(); 114 | client.moveToObject("selector", 50, 5); 115 | client.buttonUp(); 116 | ``` 117 | 118 | ###### Assert on an elements x position 119 | ```javascript 120 | client.waitForExist("selector"); 121 | const x = client.getLocation("selector", "x") // Get x or y, if not specified, an object {x, y} is returned. 122 | 123 | expect(x).toEqual(50); 124 | ``` 125 | 126 | ###### Execute custom action (in this case trigger mouseenter on an element) 127 | ```javascript 128 | client.waitForExist("selector"); 129 | client.selectorExecute("selector", function(element) { 130 | $(element).trigger("mouseenter"); 131 | }); 132 | ``` 133 | 134 | ###### Assert on url navigation 135 | ```javascript 136 | browser .url(url + path) 137 | expect(browser.getUrl()).toEqual(url + path) 138 | ``` 139 | 140 | #### *Want to become a testing Ninja?* 141 | 142 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/chimp-meteor.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | ## Getting Started 4 | 5 | Follow the [Installation](doc:installation) instructions. 6 | 7 | Note, The official Meteor guide also has some instructions on using Chimp for acceptance testing that [you can find by clicking here](http://guide.meteor.com/testing.html#acceptance-testing). 8 | 9 | Ensure that your features and tests are in a tests folder; you will encounter a `ReferenceError: module is not defined` error once you start to create your step definitions if they are not. 10 | 11 | Start your Meteor application then start Chimp: 12 | 13 | ```shell 14 | # Cucumber mode 15 | mkdir tests 16 | chimp --ddp=http://localhost:3000 --watch --path=tests 17 | 18 | # Mocha mode 19 | chimp --ddp=http://localhost:3000 --watch --mocha --path=tests 20 | ``` 21 | 22 | Chimp will now watch the `tests` directory and rerun if any of those change, or if the Meteor app restarts, so you can enjoy a lightning-fast reactive testing experience. 23 | 24 | Chimp uses Google Chrome by default so you need to install it. If you don't wish to use Chrome, you can specify the `--browser=firefox` flag. You can also choose `phantomjs` and `safari`. 25 | 26 | # Meteor Specific Features 27 | 28 | ## Hot-code reload watch mode 29 | When Meteor is detected in the `--watch` mode, Chimp will connect to your app via DDP and watch for client/server updates. As soon as any are detected, Chimp will rerun the tests/specs that have the watched tags. 30 | 31 | ## Call a DDP Method 32 | when the `--ddp` flag is passed, you get a global `server` object available in your tests/steps that allows you to call methods via DDP using this syntax: 33 | 34 | ```javascript 35 | // using call 36 | var result = server.call('fixtures/resetMyApp'); 37 | 38 | // or apply 39 | var result = server.apply('myMethodWithArgs', [argsArray]); 40 | ``` 41 | 42 | Note: for Mocha `server` is defined with in the `it`,`before`, `beforeEach`, `after` and `afterEach` callbacks. `server` is undefined in the `describe` callback. 43 | 44 | These work just like they do on the Meteor server and they are synchronous. 45 | 46 | You can define fixtures using [this method](http://guide.meteor.com/testing.html#creating-integration-test-data) and run `meteor` alongside Chimp using [this method](http://guide.meteor.com/testing.html#creating-acceptance-test-data). 47 | 48 | ## Execute Code in the Meteor Context 49 | You can run code inside the Meteor context from your specs like this: 50 | 51 | ##### In the Server Context 52 | 53 | ```javascript 54 | var getMeteorSettings = function (setting) { 55 | return Meteor.settings[setting] 56 | }; 57 | var mySetting = server.execute(getMeteorSettings, 'mySetting'); 58 | console.log(mySetting); 59 | ``` 60 | 61 | The call is synchronous, can return complex objects and works both in Cucumber and Mocha. 62 | 63 | ## Meteor Utility Packages 64 | In order for ```server.execute()``` to work, you need to install the[ xolvio:backdoor](https://atmospherejs.com/xolvio/backdoor) package. We also recommend installing the [xolvio:cleaner](https://atmospherejs.com/xolvio/cleaner) package, which allows for resetting your Mongo test database. 65 | 66 | ##### In the Client Context 67 | you can do this synchronously using WebdriverIO's [execute](http://webdriver.io/api/protocol/execute.html) 68 | 69 | ```javascript 70 | var getUserProfileProperty = function (property) { 71 | return Meteor.user().profile[property]; 72 | }; 73 | client.url('http://localhost:3000'); // the client must be on a Meteor app page 74 | var userName = client.execute(getUserProfileProperty, 'name').value; 75 | console.log(userName); 76 | ``` 77 | 78 | ## Try out the example 79 | See our [Automated Testing Best Practices](https://github.com/xolvio/automated-testing-best-practices) for examples of using Chimp. 80 | 81 | You may also be interested in the [tests/cucumber](https://github.com/xolvio/Letterpress/tree/master/app/tests/cucumber) directory of our dated - but still useful - Letterpress project. 82 | 83 | ## Learn Mocha, Cucumber & Chimp 84 | Use the links on the right hand side to learn more about Chimp, Mocha, Cucumber and other libraries used in this package. 85 | 86 | 87 | #### *Want to become a testing Ninja?* 88 | 89 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/cloud-services.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | Since Chimpy uses Selenium, you get the benefit of using all of the cloud based testing services. 4 | 5 | Here is a sample command that you could use to setup a connection with Sauce Labs: 6 | ``` 7 | chimpy --host="ondemand.saucelabs.com" --port=80 --key="SAUCE_KEY" 8 | --user="SAUCE_USER" --name="foo" 9 | ``` 10 | 11 | You can also put these settings in a configuration file like this: 12 | 13 | ```javascript 14 | module.exports = { 15 | user: "SAUCE_USER", 16 | key: "SAUCE_KEY", 17 | port: 80, 18 | host: "ondemand.saucelabs.com", 19 | name: "foo" 20 | }; 21 | ``` 22 | 23 | See the [Configuration page](/configuration.md) for more details on setting up a configuration file. 24 | 25 | 26 | ### Browser Stack Example 27 | ``` 28 | module.exports = { 29 | user: "My Browserstack Username", 30 | key: "My Browserstack Access Key", 31 | port: 80, 32 | host: "hub-cloud.browserstack.com", 33 | browser: "Chrome", 34 | 35 | // - - - - WEBDRIVER-IO - - - - 36 | webdriverio: { 37 | desiredCapabilities: { 38 | 'browserstack.local': true, 39 | 'build': 'My Build', 40 | 'project': 'My Project Name', 41 | 'os' : 'Windows', 42 | 'os_version' : '7' 43 | } 44 | }, 45 | }; 46 | ``` 47 | 48 | #### *Want to become a testing Ninja?* 49 | 50 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. 51 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | Chimpy and the integrated packages are all configurable through the command-line. Although Chimpy is somewhat opinionated, it tries to be as flexible as possible for advanced users. 4 | 5 | See the [default Chimpy configuration](https://github.com/TheBrainFamily/chimpy/blob/master/src/bin/default.js) file for the available options. You can provide any of the options in that file via the command-line also by prefixing the listed option with `--`. 6 | 7 | ```javascript 8 | // Example configuration to use browser located into non standard path 9 | module.exports = { 10 | seleniumStandaloneOptions: { 11 | seleniumArgs: [ 12 | '-Dwebdriver.firefox.bin=/Applications/FirefoxNightly.app/Contents/MacOS/firefox-bin' 13 | ] 14 | } 15 | } 16 | ``` 17 | ## Using a Configuration File 18 | If you would like to use a configuration file, you can simply place a `chimp.js` file in the directory that you run Chimpy from. 19 | 20 | You can also name your configuration file, but be sure to include the word `chimpy` inside it, and pass it in as the first parameter after the test runner, for example: 21 | 22 | ```bash 23 | chimpy config/chimp-ci.js 24 | ``` 25 | 26 | #### Using Meteor? 27 | If you are using Meteor, you will not be able to put the configuration file in the root directory of your project as Meteor will compile and run it, which results in a `module.exports` is not defined error. 28 | 29 | You can get around Create a directory such as `.config` inside your Meteor directory and you can place a configuration file in there, like this: 30 | 31 | ``` 32 | 33 | ├── .config 34 |     └── chimp.js 35 | ``` 36 | 37 | Then you can run Chimpy with: 38 | 39 | ```bash 40 | chimpy .config/chimp.js 41 | ``` 42 | 43 | Be sure the config file is the first parameter you pass to Chimpy that the config filename contains the word `chimp`. 44 | 45 | > **NOTE:** 46 | > *You need to pass arguments always with an equal sign like `--tags=@focus` to Chimp.* 47 | 48 | For Cucumber pass-through options, see here: 49 | * See [Cucumber.js CLI documentation](https://github.com/cucumber/cucumber-js#cli) 50 | * See [Cucumber.js options source code](https://github.com/cucumber/cucumber-js/blob/v0.9.2/lib/cucumber/cli.js#L24-L43) 51 | 52 | #### *Want to become a testing Ninja?* 53 | 54 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. 55 | -------------------------------------------------------------------------------- /docs/credits.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | We are thankful for the support of our community and sincerely grateful for the efforts of the contributors below: 4 | 5 | [DominikGuzei](https://github.com/DominikGuzei) 6 | Wrote the Chimp-Widget library and kindly contributed it to Xolv.io. 7 | 8 | [Łukasz Gandecki](https://github.com/lgandecki) 9 | Helped us create a parallelized version of Chimp & xolvio:cucumber for super fast end to end testing. 10 | 11 | [Eric Clifford](https://github.com/eclifford) 12 | Chimp was originally forked from his excellent [Cuked](https://github.com/eclifford/cuked) project. 13 | 14 | #### *Want to become a testing Ninja?* 15 | 16 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | You can debug your Cucumber tests or Chimpy with the node debugger. 4 | 5 | ###[Debug Chimpy](#debug-chimpy) 6 | 7 | ###[Debug Your steps (Cucumber)](#debug-cucumber-your-steps) 8 | 9 | ###[Debug your specs (Mocha)](#debug-mocha-your-specs) 10 | 11 | # Debug Chimpy 12 | 13 | ## 1. Add breakpoints 14 | 15 | Use the `debugger;` statement in your code to create a breakpoint. You can place additional breakpoints later in the debugging session with the node-inspector user interface. 16 | 17 | 18 | ## 2. Start chimpy in debug mode 19 | 20 | Start chimpy with node in debug mode. 21 | 22 | ```shell 23 | node --debug --debug-brk `which chimpy` 24 | # or for node 8.0 25 | node --inspect --inspect-brk `which chimpy` 26 | ``` 27 | 28 | Wait until you see `debugger listening on port 5858` in the console. 29 | 30 | ## 3. Start node-inspector 31 | 32 | Install node-inspector. 33 | 34 | ```shell 35 | npm install node-inspector -g 36 | ``` 37 | 38 | Start node-inspector. 39 | 40 | ```shell 41 | node-inspector 42 | ``` 43 | 44 | There will be two different debug sessions available. 45 | 46 | **Chimpy:** [http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858](http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858) 47 | **Cucumber** (and your tests): [http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5859](http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5859) 48 | 49 | First you need to open the [Chimpy debug session](http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5858). If you don't want to debug Chimpy itself, just press the resume button in the node-inspector user interface. 50 | 51 | Wait until you see `debugger listening on port 5859` in the console. Now you can open the [Cucumber debug session](http://127.0.0.1:8080/?ws=127.0.0.1:8080&port=5859). After you have set your breakpoints, press the resume button and wait until a breakpoint hits. 52 | 53 | Happy debugging! 54 | 55 | # Debug Cucumber (Your Steps) 56 | 57 | ## 1. Add breakpoints 58 | 59 | Use the `debugger;` statement in your code to create a breakpoints. You can place additional breakpoints later in the debugging session with the node-inspector user interface. 60 | 61 | 62 | ## 2. Tell Chimpy to start Cucumber in debug mode 63 | 64 | You can use these command line switched to do that: 65 | 66 | ```shell 67 | chimpy --debugCucumber 68 | # or 69 | chimpy --debugCucumber= 70 | ``` 71 | 72 | To debug mode and break on start: 73 | 74 | ```shell 75 | chimpy --debugBrkCucumber 76 | # or 77 | chimpy --debugBrkCucumber= 78 | ``` 79 | 80 | You can then connect your favorite debugger and enjoy! 81 | 82 | # Debug Mocha (Your Specs) 83 | 84 | # 1. Add breakpoints 85 | 86 | Use the `debugger;` statement in your code to create a breakpoints. You can place additional breakpoints later in the debugging session with the node-inspector user interface. 87 | 88 | 89 | ## 2. Tell Chimpy to start Mocha in debug mode 90 | 91 | You can use these command line switched to do that: 92 | 93 | ```shell 94 | chimpy --debugMocha 95 | # or 96 | chimpy --debugMocha= 97 | ``` 98 | 99 | To use debug mode and break on start use: 100 | 101 | ```shell 102 | chimpy --debugBrkMocha 103 | # or 104 | chimpy --debugBrkMocha= 105 | ``` 106 | 107 | #### *Want to become a testing Ninja?* 108 | 109 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpyRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/further-reading.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | #### Mocha Official docs 4 | * [Mocha website](https://mochajs.org/) 5 | * [Mocha repository](https://github.com/mochajs/mocha) 6 | 7 | #### WebdriverIO Official docs 8 | * [WebdriverIO website](http://webdriver.io) 9 | * [WebdriverIO repository](https://github.com/webdriverio/webdriverio/) 10 | 11 | #### Selenium Official docs 12 | * [Selenium](http://www.seleniumhq.org) 13 | * [Selenium WebDriver](http://www.seleniumhq.org/projects/webdriver/) 14 | 15 | #### Gherkin Syntax Information 16 | * [The Gherkin syntax](https://github.com/cucumber/cucumber/wiki/Gherkin) wiki pages 17 | 18 | #### Great Articles 19 | * [Behaviour Driven Development](http://lizkeogh.com/behaviour-driven-development/) by Liz Keogh 20 | * [Acceptance Criteria vs Scenarios](http://lizkeogh.com/2011/06/20/acceptance-criteria-vs-scenarios/) by Liz Keogh 21 | 22 | #### Books 23 | * [Quality, Faster.](http://quality.xolv.io/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Further&utm_campaign=QFLaunch) By Sam Hatoum, creator of Chimp. 24 | * [The Cucumber Book](https://pragprog.com/book/hwcuc/the-cucumber-book) by Matt Wynne and Aslak Hellesøy 25 | * [Specification by Example](http://www.manning.com/adzic/) by Gojko Adzic (Use this code: cukes38sbe for 38% off) 26 | 27 | #### Official Resources 28 | * [BDD Google Group](https://groups.google.com/forum/#!forum/behaviordrivendevelopment) 29 | * [Cucumber School](https://cukes.info/school) 30 | * [Cucumber Blog](https://cucumber.io/blog) 31 | 32 | #### *Want to become a testing Ninja?* 33 | 34 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/getting-started-cucumber.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | Follow the [Installation](/installation) instructions here. 4 | 5 | # 1. Develop 6 | 7 | Chimpy assumes you have a folder called `./features` in the directory you run it from where your CucumberJS feature and support files reside. If you don't have any feature files or don't know what that is, please see the [Key Concepts section](doc:key-concepts) below and then run through the [tutorial](doc:tutorial) to get up to speed. 8 | 9 | ```bash 10 | chimpy --watch 11 | ``` 12 | 13 | Chimpy will watch your feature & supporting files directory and rerun any features/scenarios that have the tag `@watch`. 14 | 15 | By default, Chimp starts a Google Chrome browser window. This is where your specs will run and you can use this window to see the automation as it happens. Chimp also ensures the same window will remain open between your specs rerunning so you won't get an annoying window popup. 16 | 17 | The watch mode is designed to keep you focused on the task at hand. The idea is you work on a scenario through to completion, and then move the `@watch` tag to the next scenario. 18 | 19 | 20 | # 2. Test 21 | 22 | You've now finished your tasks and are ready to commit your code. If you want to run the build locally you can run all the tests using this command: 23 | 24 | ```bash 25 | chimpy --browser=firefox 26 | ``` 27 | 28 | Chimpy recognizes that you're not in watch mode and will run all specs, except those with the 29 | `@ignore` tag. 30 | 31 | Notice how you can use a different browser. It's a good practice to develop using one browser and run all tests with another. 32 | 33 | This same command can also be used on CI servers. You can tell Chimp to output the CucumberJS report to a JSON file like this: 34 | 35 | ```bash 36 | chimpy --jsonOutput=cucumber_output.json 37 | ``` 38 | 39 | This is a good build artifact to keep for traceability, especially on CircleCI where these files are natively supported. 40 | 41 | #### *Need Help?* 42 | Contact us the maintainers of Chimpy. The Brain [offers testing consulting and training services](TODO) that can help you speed up your development and improve the quality of your products and services. 43 | 44 | #### *Want to learn more?* 45 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/getting-started-jasmine.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | Follow the [Installation](/installation) instructions here. 4 | 5 | # 1. Develop 6 | 7 | To start developing using Jasmine, you have to start Chimp with the `--jasmine` flag like this: 8 | 9 | ```bash 10 | chimpy --jasmine --watch 11 | ``` 12 | 13 | Chimpy will watch your features directory for any `.js` files and rerun any `describe`/`it` blocks that contain the string `@watch`. For example: 14 | 15 | ```javascript 16 | describe('Chimp Mocha', function() { 17 | describe('Page title', function () { 18 | it('should be set by the Meteor method @watch', function () { 19 | browser.url('http://www.google.com'); 20 | expect(browser.getTitle()).to.equal('Google'); 21 | }); 22 | }); 23 | }); 24 | ``` 25 | 26 | By default, Chimpy starts a Google Chrome browser window. This is where your specs will run and you can use this window to see the automation as it happens. Chimpy also ensures the same window will remain open between your specs rerunning so you won't get an annoying window popup. 27 | 28 | The watch mode is designed to keep you focused on the task at hand. The idea is you work on a scenario through to completion, and then move the `@watch` tag to the next spec you are working on. 29 | 30 | 31 | # 2. Test 32 | 33 | You've now finished your tasks and are ready to commit your code. If you want to run the build locally you can run all the tests using this command: 34 | 35 | ```bash 36 | chimp --mocha --browser=firefox 37 | ``` 38 | 39 | Chimpy recognizes that you're not in watch mode and will run all the specs. 40 | 41 | Notice how you can use a different browser. It's a good practice to develop using one browser and run all tests with another. 42 | 43 | This same command can also be used on CI servers. 44 | 45 | #### *Want to become a testing Ninja?* 46 | 47 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. 48 | -------------------------------------------------------------------------------- /docs/getting-started-mocha.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | Follow the [Installation](installation) instructions here. 4 | 5 | # 1. Develop 6 | 7 | To start developing using Mocha, you have to start Chimp with the `--mocha` flag like this: 8 | 9 | ```bash 10 | chimpy --mocha --watch 11 | ``` 12 | 13 | Chimpy will watch your feature directory for any `.js` files and rerun any `describe`/`it` blocks that contain the string `@watch`. For example: 14 | 15 | ```javascript 16 | describe('Chimp Jasmine', function() { 17 | describe('Page title', function () { 18 | it('should be set by the Meteor method @watch', function () { 19 | browser.url('http://www.google.com'); 20 | expect(browser.getTitle()).toEqual('Google'); 21 | }); 22 | }); 23 | }); 24 | ``` 25 | 26 | Make sure that your spec files end with "_spec", "-spec" or "Spec". 27 | 28 | By default, Chimpy starts a Google Chrome browser window. This is where your specs will run and you can use this window to see the automation as it happens. Chimpy also ensures the same window will remain open between your specs rerunning so you won't get an annoying window popup. 29 | 30 | The watch mode is designed to keep you focused on the task at hand. The idea is you work on a scenario through to completion, and then move the `@watch` tag to the next spec you are working on. 31 | 32 | # 2. Test 33 | 34 | You've now finished your tasks and are ready to commit your code. If you want to run the build locally you can run all the tests using this command: 35 | 36 | ```bash 37 | chimpy --jasmine 38 | ``` 39 | 40 | Chimpy recognizes that you're not in watch mode and will run all the specs. 41 | 42 | This same command can also be used on CI servers. 43 | 44 | # 3. Learn More 45 | 46 | You can find more info on the [Jasmine support](/jasmine-support.md) page. 47 | 48 | 49 | #### *Want to become a testing Ninja?* 50 | 51 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/images/ci.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBrainFamily/chimpy/92188d3a7c485ed661baf3534b5ec5ee8b651e8d/docs/images/ci.png -------------------------------------------------------------------------------- /docs/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBrainFamily/chimpy/92188d3a7c485ed661baf3534b5ec5ee8b651e8d/docs/images/header.png -------------------------------------------------------------------------------- /docs/images/meteor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBrainFamily/chimpy/92188d3a7c485ed661baf3534b5ec5ee8b651e8d/docs/images/meteor.png -------------------------------------------------------------------------------- /docs/images/realtime.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBrainFamily/chimpy/92188d3a7c485ed661baf3534b5ec5ee8b651e8d/docs/images/realtime.gif -------------------------------------------------------------------------------- /docs/images/test-frameworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBrainFamily/chimpy/92188d3a7c485ed661baf3534b5ec5ee8b651e8d/docs/images/test-frameworks.png -------------------------------------------------------------------------------- /docs/images/wdio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBrainFamily/chimpy/92188d3a7c485ed661baf3534b5ec5ee8b651e8d/docs/images/wdio.png -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | ## Prerequisite 4 | 5 | If you would like to use Selenium, you need to have Java. 6 | **Oracle JDK v1.8+** ([Download Here](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)) 7 | You can check your Java version in the terminal with `java -version` (single dash) 8 | 9 | Be sure to install the **JDK** not the JRE. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install chimpy 15 | ``` 16 | 17 | and then you can run using `./node_modules/.bin/chimpy` 18 | 19 | ## Gulp Installation 20 | 21 | TODO: This needs a patch since Chimpy has been forked from Chimp. 22 | 23 | [gulp-chimpy](https://www.npmjs.com/package/gulp-chimpy) is a wrapper to interact with Chimp.js in a gulp task. 24 | 25 | ## Quick Install 26 | 27 | In the terminal run the following command 28 | ``` 29 | npm install gulp-chimpy --save-dev 30 | ``` 31 | 32 | ## Usage with chimp.conf.js file [chimp.conf.js](https://github.com/eduardogch/gulp-chimp/blob/master/chimp.conf.js) 33 | 34 | ``` 35 | var chimp = require('gulp-chimp'); 36 | 37 | /* Chimp.js - Automated/e2e Testing with a config file */ 38 | gulp.task('chimpy', function () { 39 | return chimp('./chimp.conf.js'); 40 | }); 41 | ``` 42 | 43 | ## Cucumber HTML Report 44 | 45 | ![alt tag](https://github.com/eduardogch/gulp-chimp/raw/master/cucumber-html-report.png) 46 | 47 | ## Usage with chimp.js options 48 | 49 | ``` 50 | /* Chimp.js - Automated/e2e Testing with options */ 51 | gulp.task('chimp-options', function () { 52 | return chimp({ 53 | path: './source/e2e/features', // Cucumber features files 54 | browser: 'phantomjs', 55 | debug: true, 56 | singleRun: false, 57 | log: 'info', 58 | timeout: 60000, 59 | port: 2345, 60 | reportHTML: true 61 | }); 62 | }); 63 | ``` 64 | 65 | ##### path 66 | 67 | Type: `string` 68 | Default: `./source/e2e/features` 69 | 70 | ##### browser 71 | 72 | Type: `string` 73 | Default `chrome` 74 | 75 | ##### singleRun 76 | 77 | Type: `boolean` 78 | Default `true` 79 | 80 | ##### debug 81 | 82 | Type: `boolean` 83 | Default `false` 84 | 85 | ##### log 86 | 87 | Type: `string` 88 | Default: `info` 89 | 90 | ##### timeout 91 | 92 | Type: `number` 93 | Default `60000` 94 | 95 | ##### port 96 | 97 | Type: `number` 98 | Default `2356` 99 | 100 | ##### reportHTML 101 | 102 | Type: `boolean` 103 | Default `true` 104 | 105 | 106 | ## Troubleshooting 107 | **Permission Denied** 108 | If you get this error message: 109 | `Error: EACCES: permission denied, mkdir...` 110 | 111 | Try deleting the .selenium directory using: 112 | ```bash 113 | sudo rm -rf /usr/local/lib/node_modules/chimpy/node_modules/selenium-standalone/.selenium 114 | ``` 115 | 116 | **Failed at the fibers** 117 | If you get this error message: 118 | `npm ERR! Failed at the fibers@1.0.9 install script 'node build.js || nodejs build.js'.` 119 | 120 | Upgrade to Node 4.x+ 121 | 122 | 123 | #### *Want to become a testing Ninja?* 124 | 125 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/jasmine-support.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | Chimpy supports the latest version of Jasmine. Currently this is version 2.4.x. 4 | 5 | You can enable Jasmine support by starting Chimpy with with `--jasmine` flag or setting `jasmine: true` in your Chimpy config file. 6 | 7 | Additionally the Chimpy config file supports the [following Jasmine options](https://github.com/TheBrainFamily/chimpy/blob/master/src/bin/default.js#L120-L136). The values in this file are the defaults. You can read more about those options in the [Jasmine documentation](http://jasmine.github.io/2.4/node.html). 8 | 9 | Additionally by default: 10 | * Spec file names need to end with "_spec.js", "-spec.js" or "Spec.js". 11 | * Files in the support/ folder will run before the tests. 12 | * You can use ES2015 out of the box. 13 | 14 | If you miss any information or something is unclear please [open an issue](https://github.com/xolvio/chimp/issues/new). 15 | 16 | ### Custom spec filter 17 | 18 | Add a helper file with something like for example: 19 | 20 | ```javascript 21 | const smokeTestRegExp = /@smoke/; 22 | jasmine.addSpecFilter(function (spec) { 23 | return smokeTestRegExp.test(spec.getFullName()); 24 | }); 25 | ``` -------------------------------------------------------------------------------- /docs/migrate-to-synchronous.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | If you have any trouble with the migration, let us know via [GitHub issue](https://github.com/TheBrainFamily/chimpy/issues) 4 | 5 | # 1. Start Chimpy with "--sync=false" 6 | 7 | Start chimpy with `chimpy --sync=false --watch` (add your other options). 8 | 9 | This is you step definition before the migration: 10 | 11 | ```javascript 12 | // Promise Style: 13 | this.When(/^I visit "([^"]*)"$/, function (url) { 14 | return client.url(url); 15 | }); 16 | ``` 17 | 18 | ```javascript 19 | // Callback Style: 20 | this.When(/^I visit "([^"]*)"$/, function (url, callback) { 21 | browser.url(url).call(callback); 22 | }); 23 | ``` 24 | 25 | # 2. Migrate all step definitions to synchronous style one by one 26 | 27 | Use the WebDriver.io commands with the Sync postfix until you have migrated all step definitions to the synchronous style. This way you continuously get feedback from Cucumber if you make any migration errors. 28 | 29 | ```javascript 30 | this.When(/^I visit "([^"]*)"$/, function (url) { 31 | client.urlSync(url); 32 | }); 33 | ``` 34 | 35 | We removed the callback parameter, no longer return anything and use `urlSync`. 36 | 37 | # 3. Remove all Sync postfixes 38 | 39 | After you have migrated all step definitions to the synchronous style, you can remove all Sync postfixes. 40 | 41 | Use search and replace with a regular expression. Search for `(?:this\.)?((?:browser|client|driver|server|ddp)\.\w+)Sync` and replace it with `$1`. It removes also the `this.` prefix. 42 | 43 | ```javascript 44 | this.When(/^I visit "([^"]*)"$/, function (url) { 45 | client.url(url); 46 | }); 47 | ``` 48 | 49 | # 4. Start Chimpy without "--sync=false" 50 | 51 | Finally, start Chimp without `--sync=false` (the default is true). 52 | 53 | #### *Want to become a testing Ninja?* 54 | 55 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/multi-browser-testing.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | To test using multiple browsers you need to use environment variable like so: 4 | 5 | ```shell 6 | export CUCUMBER_BROWSERS=2 7 | ``` 8 | 9 | This will expose the browser instances in global browser.instances variable. There are different ways of using this, but here is some code example for inspiration: 10 | 11 | ```javascript 12 | var browsers; 13 | 14 | this.Before(function() { 15 | browsers = { 16 | Alice: browser.instances[0], 17 | Bob: browser.instances[1] 18 | }; 19 | }) 20 | 21 | this.Given(/^([^ ]*) go(?:es)? to "([^"]*)"$/, function (person, relativePath) { 22 | getBrowserFor(person).url(url.resolve(process.env.ROOT_URL, relativePath)); 23 | }); 24 | 25 | this.Then(/^([^ ]*) sees? "([^"]*)" label$/, function (person, label) { 26 | var _labelSelector = "//*//label[text()='" + label + "']"; 27 | expect(getBrowserFor(person).isVisible(_labelSelector)).toBe(true); 28 | }); 29 | 30 | 31 | function getBrowserFor(person) { 32 | return (person === 'Both') ? browser : browsers[person]; 33 | } 34 | ``` 35 | 36 | Then you can use Bob and Alice in your .feature files: 37 | 38 | ```gherkin 39 | Scenario: Login into the system 40 | Given Both go to "/" 41 | Then Alice sees "Very nice" label 42 | And Bob sees "Very nice" label 43 | ``` 44 | 45 | In production code you'd probably want to make the browsers variable global and expose it to your step definitions. 46 | -------------------------------------------------------------------------------- /docs/reporting.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | With Chimpy you can specify the reporter used to generate test results in each of the integrated frameworks. 4 | 5 | ### Mocha 6 | 7 | If you are using Mocha, you can set the ```mochaReporter``` option in your chimp configuration to any of the official reporters listed [here](https://mochajs.org/#reporters) or one that you have installed from NPM. 8 | 9 | From the command line: 10 | 11 | ```bash 12 | chimpy --mocha --mochaReporter="nyan" 13 | ``` 14 | 15 | Or from your configuration file: 16 | 17 | ```json 18 | { 19 | mocha: true, 20 | mochaReporter: "progress" 21 | } 22 | ``` 23 | 24 | Due to some limitations in the Mocha framework, you are only able to use one reporter per test run. One workaround for this is to combine reporters, take a look at an example of this [here](https://github.com/sandcastle/mocha-circleci-reporter). 25 | 26 | #### *Want to become a testing Ninja?* 27 | 28 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /docs/synchronous-tests.md: -------------------------------------------------------------------------------- 1 | | **[Home](/chimpy)** | 2 | 3 | With Chimpy you can write your tests in a synchronous style. So you don't have to deal with confusing promise chains or callback hell. 4 | 5 | All the APIs that are shipped with Chimp can be used in a synchronous style by default. This includes the WebdriverIO commands, request module, and the DDP client. 6 | 7 | ## Making asynchronous functions synchronous 8 | 9 | If you have asynchronous functions that you want to use in your tests, you can wrap them with our helper functions `wrapAsync` and `wrapAsyncObject`. Both helpers work with asynchronous functions that return a promise or use a callback. 10 | 11 | * **wrapAsync(fn: Function, [context: Object]): Function** 12 | Takes the asynchronous function as first argument and optional the function context (the value of `this` inside the function). It returns a synchronous version of the asynchronous function. 13 | 14 | * **wrapAsyncObject(object: Object, properties: Array, [options: Object]): Object** 15 | Creates synchronous versions of the asynchronous methods of an object. It returns the wrapper object with the synchronous and asynchronous methods.The first argument is the object. The second argument is an array of the method names that should be made synchronous. 16 | 17 | ### Example 18 | 19 | You have an object `myObject` with an asynchronous method `myFunc`: 20 | 21 | ```javascript 22 | var myObject = { 23 | myFunc: function (callback) { ... } // Asynchronous function 24 | } 25 | ``` 26 | 27 | You wrap it with `wrapAsyncObject`: 28 | 29 | ```javascript 30 | var myObjectWrapper = wrapAsyncObject(myObject, ['myFunc']) 31 | ``` 32 | 33 | `myObjectWrapper` is: 34 | 35 | ```javascript 36 | myObjectWrapper = { 37 | myFunc: function () { ... } // Synchronous version 38 | myFuncSync: function () { ... } // Synchronous version 39 | myFuncAsync: function (callback) { ... } // Asynchronous version 40 | } 41 | ``` 42 | 43 | myObject has not changed. 44 | 45 | ### options 46 | 47 | * **syncByDefault: Boolean (default: true)** 48 | 49 | If you set this to `false`, you get this result instead: 50 | 51 | ```javascript 52 | myObjectWrapper = { 53 | myFunc: function (callback) { ... } // Asynchronous version 54 | myFuncSync: function () { ... } // Synchronous version 55 | myFuncAsync: function (callback) { ... } // Asynchronous version 56 | } 57 | ``` 58 | 59 | `myFunc` is asynchronous. 60 | 61 | * **wrapAsync: Function (default: wrapAsync)** 62 | 63 | If you want to use another function to wrap the asynchronous methods, you could pass it with this option. 64 | 65 | #### *Want to become a testing Ninja?* 66 | 67 | Checkout Xolv.io's new [Quality Faster](https://www.qualityfaster.com/?utm_source=XolvOSS&utm_medium=OSSDocs&utm_content=ChimpRM-Home&utm_campaign=QFLaunch) guide where you can learn how to can bake quality in across the full stack using React, Node.JS, Jest, Meteor and more. -------------------------------------------------------------------------------- /features/es2015.feature: -------------------------------------------------------------------------------- 1 | Feature: ES2015 in step definitions 2 | 3 | As a developer 4 | I want to write step definitions with ES2015 5 | So that I can use all the new ES2015 features 6 | 7 | Scenario: Use ES2015 8 | # TODO: This step should generate a new project that uses Chimp 9 | When I use ES2015 10 | Then it works 11 | -------------------------------------------------------------------------------- /features/step_definitions/es2015-steps.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | this.When(/^I use ES2015$/, function () { 4 | const {a, b} = {a: 'foo', b: 'bar'}; 5 | const arrowFunc = (foo) => foo; 6 | class Foo { 7 | constructor() {} 8 | foo() {} 9 | } 10 | var object = { 11 | foo() {} 12 | }; 13 | const templateString = `Foo`; 14 | const [c, ,d] = [1,2,3]; 15 | }); 16 | 17 | this.When(/^it works$/, function () {}); 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /features/step_definitions/webdriver-steps.js: -------------------------------------------------------------------------------- 1 | var myStepDefinitionsWrapper = function () { 2 | 3 | this.When(/^I visit "([^"]*)"$/, function (url) { 4 | browser.url(url); 5 | }); 6 | 7 | this.Then(/^I see the title of "([^"]*)"$/, function (title) { 8 | expect(browser.getTitle()).toEqual(title); 9 | }); 10 | 11 | }; 12 | module.exports = myStepDefinitionsWrapper; 13 | -------------------------------------------------------------------------------- /features/webdriver.feature: -------------------------------------------------------------------------------- 1 | Feature: Use browser inside steps 2 | 3 | As a developer 4 | I want to have webdriver available to me in my steps 5 | So that I don't have to configure my world object and I focus on testing 6 | 7 | @watch 8 | Scenario: Visit Google 9 | When I visit "http://www.google.com" 10 | Then I see the title of "Google" 11 | 12 | @not-watch 13 | Scenario: Visit Github 14 | When I visit "http://www.github.com" 15 | Then I see the title of "The world’s leading software development platform · GitHub" 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index'); 2 | -------------------------------------------------------------------------------- /mocha-setup.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const chai = require('chai'); 3 | chai.should(); 4 | global.expect = chai.expect; 5 | global.assert = chai.assert; 6 | 7 | const td = require('testdouble'); 8 | const quibble = require('quibble'); 9 | global.td = td; 10 | 11 | beforeEach(() => { 12 | td.reset(); 13 | quibble.ignoreCallsFromThisFile(require.main.filename); 14 | }); 15 | })(); 16 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:@babel/register 2 | --reporter spec 3 | --ui bdd 4 | mocha-setup.js 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chimpy", 3 | "version": "3.0.4", 4 | "description": "Develop acceptance tests & end-to-end tests with realtime feedback.", 5 | "keywords": [ 6 | "simian", 7 | "meteor", 8 | "bdd", 9 | "atdd", 10 | "cucumber", 11 | "webdriverio", 12 | "selenium", 13 | "phantom", 14 | "testing", 15 | "saucelabs" 16 | ], 17 | "author": "Sam Hatoum (http://xolv.io)", 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/TheBrainFamily/chimpy" 22 | }, 23 | "homepage": "https://github.com/TheBrainFamily/chimpy#readme", 24 | "bugs": { 25 | "url": "https://github.com/TheBrainFamily/chimpy/issues" 26 | }, 27 | "watch": { 28 | "prepare": "src/{lib,bin}/**/*.js" 29 | }, 30 | "scripts": { 31 | "watch": "npm-watch", 32 | "prepare": "npx babel src --ignore spec,__tests__/options-loader.js --out-dir ./dist", 33 | "lint": "eslint ./src", 34 | "lintfix": "eslint ./src --fix", 35 | "start": "node ./scripts/run.js", 36 | "testonly": "npm run testunit && npm run prepare && node ./scripts/run-tests.js", 37 | "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", 38 | "test": "npm run testonly", 39 | "publish-patch": "./scripts/release.sh patch", 40 | "publish-minor": "./scripts/release.sh minor", 41 | "publish-major": "./scripts/release.sh major" 42 | }, 43 | "main": "dist/lib/chimp.js", 44 | "bin": { 45 | "chimp": "./bin/chimp.js", 46 | "chimpy": "./bin/chimpy.js" 47 | }, 48 | "mocha": { 49 | "files": [ 50 | "src/**/*.js", 51 | "!src/__mocks__/**/*", 52 | "!src/__tests__/**/*", 53 | "!src/**/*-spec.js" 54 | ], 55 | "tests": [ 56 | "src/lib/**/*-spec.js" 57 | ] 58 | }, 59 | "jest": { 60 | "testRunner": "jasmine2", 61 | "transform": { 62 | "^.+\\.js$": "babel-jest" 63 | }, 64 | "moduleFileExtensions": [ 65 | "js", 66 | "json", 67 | "node" 68 | ], 69 | "unmockedModulePathPatterns": [ 70 | "core-js/.*", 71 | "babel-runtime/.*" 72 | ] 73 | }, 74 | "dependencies": { 75 | "@babel/cli": "^7.8.4", 76 | "@babel/core": "^7.8.4", 77 | "@babel/plugin-transform-runtime": "^7.8.3", 78 | "@babel/polyfill": "^7.8.3", 79 | "@babel/preset-env": "^7.8.4", 80 | "@babel/register": "^7.8.3", 81 | "@babel/runtime": "^7.8.4", 82 | "async": "~0.9.0", 83 | "bluebird": "^3.5.4", 84 | "chai": "~4.1.2", 85 | "chai-as-promised": "^6.0.0", 86 | "child-process-debug": "0.0.7", 87 | "chokidar": "^2.1.6", 88 | "colors": "1.1.2", 89 | "commander": "^2.20.0", 90 | "cucumber": "github:TheBrainFamily/cucumber-js#v1.3.0-chimp.7", 91 | "deep-extend": "^0.6.0", 92 | "exit": "^0.1.2", 93 | "fibers": "^3.1.0", 94 | "freeport": "~1.0.5", 95 | "fs-extra": "^1.0.0", 96 | "glob": "github:lucetius/node-glob#chimp", 97 | "hapi": "8.8.0", 98 | "jasmine": "^2.99.0", 99 | "jsonfile": "^4.0.0", 100 | "loglevel": "~1.4.0", 101 | "minimist": "~1.2.0", 102 | "mocha": "^4.1.0", 103 | "phantomjs-prebuilt": "2.1.15", 104 | "progress": "^1.1.8", 105 | "request": "^2.88.0", 106 | "requestretry": "1.5.0", 107 | "saucelabs": "^1.5.0", 108 | "selenium-standalone": "^6.16.0", 109 | "underscore": "~1.8.3", 110 | "xolvio-ddp": "^0.12.0", 111 | "xolvio-jasmine-expect": "^1.0.0", 112 | "xolvio-sync-webdriverio": "10.0.0" 113 | }, 114 | "devDependencies": { 115 | "@babel/cli": "^7.8.4", 116 | "@babel/core": "^7.8.4", 117 | "@babel/plugin-proposal-class-properties": "^7.8.3", 118 | "@babel/plugin-proposal-export-default-from": "^7.8.3", 119 | "babel-jest": "^25.1.0", 120 | "chromedriver": "^80.0.1", 121 | "eslint": "^3.12.2", 122 | "eslint-config-airbnb": "^13.0.0", 123 | "eslint-plugin-babel": "^4.0.0", 124 | "eslint-plugin-import": "^2.17.2", 125 | "eslint-plugin-jsx-a11y": "^2.2.3", 126 | "eslint-plugin-react": "^6.8.0", 127 | "git-release-notes": "0.0.2", 128 | "jest": "^25.1.0", 129 | "npm-watch": "^0.1.6", 130 | "quibble": "^0.5.7", 131 | "shelljs": "^0.7.5", 132 | "testdouble": "^3.12.5" 133 | }, 134 | "peerDependencies": { 135 | "chromedriver": "*" 136 | }, 137 | "directories": { 138 | "doc": "docs", 139 | "test": "tests" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /scripts/lib/exec.js: -------------------------------------------------------------------------------- 1 | var shell = require('shelljs'); 2 | 3 | module.exports = function exec(command, options) { 4 | if (isWindows()) { 5 | command = 'powershell.exe -Command "' + command + '"'; 6 | } 7 | return shell.exec(command, options); 8 | } 9 | 10 | function isWindows() { 11 | return process.platform === 'win32'; 12 | } 13 | -------------------------------------------------------------------------------- /scripts/release-notes.ejs: -------------------------------------------------------------------------------- 1 | <% commits.forEach(function (commit) { %> 2 | * <%= commit.title %> (<%= commit.authorName %>) <% }) %> 3 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | scriptDir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | semvar="$1" 6 | case $semvar in 7 | patch) 8 | echo patch 9 | ;; 10 | minor) 11 | echo minor 12 | ;; 13 | major) 14 | echo major 15 | ;; 16 | prepatch) 17 | echo prepatch 18 | ;; 19 | preminor) 20 | echo preminor 21 | ;; 22 | premajor) 23 | echo premojor 24 | ;; 25 | prerelease) 26 | echo prerelease 27 | ;; 28 | *) 29 | echo "Usage: release patch|minor|major|prepatch|preminor|premajor|prerelease" 30 | exit 1 31 | ;; 32 | esac 33 | currentVersion=`node -e "console.log(require('$scriptDir/../package.json').version)"` 34 | 35 | ./node_modules/.bin/git-release-notes v$currentVersion..master ./scripts/release-notes.ejs >> ./commits.md 36 | 37 | npm version $semvar 38 | npm publish 39 | 40 | newVersion=`node -e "console.log(require('$scriptDir/../package.json').version)"` 41 | 42 | echo "# $newVersion" > tempHistory.md 43 | cat ./commits.md >> ./tempHistory.md 44 | cat ./HISTORY.md >> ./tempHistory.md 45 | cat ./tempHistory.md > ./HISTORY.md 46 | rm ./commits.md 47 | rm ./tempHistory.md 48 | 49 | git add . 50 | git commit -m "Adds the release notes for v$newVersion" 51 | git push 52 | git push --tags 53 | -------------------------------------------------------------------------------- /scripts/run-tests.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('shelljs/global'); 4 | var exec = require('./lib/exec'); 5 | 6 | var nodeIndex = parseInt(env.CIRCLE_NODE_INDEX, 10); 7 | var isCI = !isNaN(nodeIndex); 8 | 9 | var run = function (runOnNodeIndex, name, command) { 10 | if (!isCI || nodeIndex === runOnNodeIndex) { 11 | echo(name); 12 | if (exec(command).code !== 0) { 13 | exit(1); 14 | } 15 | } 16 | }; 17 | 18 | var unitTestsCommand = './node_modules/.bin/jest'; 19 | if (isCI) { 20 | // Prevent exceeding the maximum RAM. Each worker needs ~385MB. 21 | unitTestsCommand += ' --maxWorkers 4'; 22 | } 23 | run(0, 'Running Chimp Unit tests', unitTestsCommand); 24 | run(0, 'Running Chimp Mocha specs in Chrome', 'node ./bin/chimp.js --mocha --path=tests/mocha'); 25 | run(0, 'Running Chimp Jasmine specs in Chrome', 'node ./bin/chimp.js --jasmine --path=tests/jasmine'); 26 | run(0, 'Running Chimp Cucumber tests', 'node ./bin/chimp.js --path=tests/cucumber'); 27 | 28 | 29 | if (isCI) { 30 | run(1, 'Running Chimp Cucumber specs in Chrome', 'node ./bin/chimp.js --tags=~@cli --simianRepositoryId=' + env.SIMIAN_REPOSITORY + ' --simianAccessToken=' + env.SIMIAN_ACCESS_TOKEN); 31 | } else { 32 | run(1, 'Running Chimp Cucumber specs in Chrome', 'node ./bin/chimp.js --tags=~@cli'); 33 | } 34 | 35 | run(2, 'Running Chimp Cucumber specs in Firefox', 'node ./bin/chimp.js --browser=firefox --tags=~@cli'); -------------------------------------------------------------------------------- /scripts/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var exec = require('./lib/exec'); 4 | 5 | exec('npm run prepare'); 6 | exec('node ./bin/chimpy.js'); 7 | -------------------------------------------------------------------------------- /src/__mocks__/.babelignore: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/__mocks__/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-transform-runtime"], 3 | "presets": ["@babel/preset-env"] 4 | } 5 | -------------------------------------------------------------------------------- /src/__mocks__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "jest": false, 4 | "jasmine": false, 5 | "spyOn": false, 6 | "it": false, 7 | "describe": false, 8 | "expect": false, 9 | "after": false, 10 | "before": false, 11 | "beforeAll": false, 12 | "beforeEach": false, 13 | "afterAll": false, 14 | "afterEach": false, 15 | "xit": false, 16 | "xdescribe": false, 17 | "fit": false, 18 | "fdescribe": false, 19 | "pending": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__mocks__/bluebird.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | jest.autoMockOff(); 4 | module.exports = require.requireActual('bluebird'); 5 | jest.autoMockOn(); 6 | -------------------------------------------------------------------------------- /src/__mocks__/chokidar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chokidar = { 4 | watcher: { 5 | on: jest.fn(), 6 | once: jest.fn() 7 | }, 8 | watch: jest.fn() 9 | }; 10 | chokidar.watch.mockReturnValue(chokidar.watcher); 11 | 12 | module.exports = chokidar; 13 | -------------------------------------------------------------------------------- /src/__mocks__/colors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | supportsColor: {} 5 | }; 6 | -------------------------------------------------------------------------------- /src/__mocks__/ddp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = jest.fn(); 4 | -------------------------------------------------------------------------------- /src/__mocks__/freeport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = jest.fn(); 4 | -------------------------------------------------------------------------------- /src/__mocks__/hapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hapi = { 4 | instance: { 5 | connection: jest.fn(), 6 | route: jest.fn(), 7 | start: jest.fn(), 8 | }, 9 | Server: function () { 10 | return Hapi.instance; 11 | } 12 | }; 13 | 14 | module.exports = Hapi; 15 | -------------------------------------------------------------------------------- /src/__mocks__/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | post: jest.fn() 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /src/__mocks__/selenium-standalone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | install: jest.fn(), 5 | start: jest.fn() 6 | }; 7 | 8 | -------------------------------------------------------------------------------- /src/__mocks__/webdriverio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | remote: jest.fn() 5 | }; 6 | -------------------------------------------------------------------------------- /src/__mocks__/xolvio-sync-webdriverio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | wrapAsyncObject: jest.fn() 5 | }; 6 | -------------------------------------------------------------------------------- /src/__tests__/.babelignore: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/__tests__/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "jest": false, 4 | "jasmine": false, 5 | "spyOn": false, 6 | "it": false, 7 | "describe": false, 8 | "expect": false, 9 | "after": false, 10 | "before": false, 11 | "beforeAll": false, 12 | "beforeEach": false, 13 | "afterAll": false, 14 | "afterEach": false, 15 | "xit": false, 16 | "xdescribe": false, 17 | "fit": false, 18 | "fdescribe": false, 19 | "pending": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/browserstack-manager-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/browserstack-manager'); 2 | 3 | describe('BrowserStack Session Manager', function () { 4 | 5 | describe('Constructor', function () { 6 | 7 | it('sets the browserStackUrl', function () { 8 | var BrowserStackManager = require('../lib/browserstack-manager'); 9 | var options = {port: 1234, browser: 'something', user: 'testus3r', key: '12345678', host: 'browserstack.com'}; 10 | 11 | var browserStackBaseUrl = 'https://' + options.user + ':' + options.key + '@' + options.host; 12 | 13 | var session = new BrowserStackManager(options); 14 | 15 | expect(session.options.browserStackUrl).toBe(browserStackBaseUrl); 16 | }); 17 | 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/ddp-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/ddp.js'); 2 | 3 | describe('DDP Wrapper', function () { 4 | var DDP = require('../lib/ddp'); 5 | describe('constructor', function () { 6 | beforeEach(function () { 7 | delete process.env.ROOT_URL; 8 | for(var key in process.env) { 9 | if(key.indexOf('chimp.ddp') !== -1) { 10 | delete process.env[key]; 11 | } 12 | } 13 | }); 14 | it('sets ROOT_URL env var to be the chimp.ddp env var', function () { 15 | process.env['chimp.ddp0'] = 'http://here.com:3000'; 16 | new DDP(); 17 | expect(process.env.ROOT_URL).toEqual('http://here.com:3000'); 18 | }); 19 | it('does not change the ROOT_URL when it is provided', function () { 20 | process.env.ROOT_URL = 'http://somewhere.com:3000'; 21 | process.env['chimp.ddp0'] = 'http://here.com:3000'; 22 | new DDP(); 23 | expect(process.env.ROOT_URL).toEqual('http://somewhere.com:3000'); 24 | }); 25 | it('parses the DDP host of [chimp.ddp0] if no url provided', function () { 26 | process.env['chimp.ddp0'] = 'http://here.com:3000'; 27 | var ddp = new DDP(); 28 | expect(ddp.url.host).toEqual('here.com:3000'); 29 | }); 30 | it('parses the DDP host of provided url', function () { 31 | process.env['chimp.ddp0'] = 'http://here.com:3000'; 32 | process.env['chimp.ddp1'] = 'http://here.com:3001'; 33 | var ddp = new DDP('http://here.com:3001'); 34 | expect(ddp.url.host).toEqual('here.com:3001'); 35 | }); 36 | }); 37 | describe('connect', function () { 38 | it('returns an async-wrapped DDPClient', function () { 39 | // TODO check that the DDPClient return value is passed to wrapAsyncObject 40 | // and that the connect', 'call', 'apply', 'callWithRandomSeed', 'subscribe' methods are passed in 41 | }); 42 | it('does not set sync-by-default when chimp.sync is false', function () { 43 | // TODO 44 | }); 45 | }); 46 | describe('_getUrl', function () { 47 | it('throws an error if http or https are not passed', function () { 48 | var thrower = function () { 49 | new DDP()._getUrl('blah.com'); 50 | }; 51 | expect(thrower).toThrowError('[chimp][ddp] DDP url must contain the protocol'); 52 | }); 53 | it('parses http URLs', function () { 54 | var url = new DDP()._getUrl('http://somewhere:3000'); 55 | expect(url.hostname).toEqual('somewhere'); 56 | expect(url.port).toEqual('3000'); 57 | expect(url.protocol).toEqual('http:'); 58 | }); 59 | it('parses https URLs', function () { 60 | var url = new DDP()._getUrl('https://somewhere:3000'); 61 | expect(url.hostname).toEqual('somewhere'); 62 | expect(url.port).toEqual('3000'); 63 | expect(url.protocol).toEqual('https:'); 64 | }); 65 | }); 66 | describe('_getOptions', function () { 67 | it('sets the port and hostname using the instance url object', function () { 68 | var ddp = new DDP(); 69 | ddp.url = { 70 | hostname: 'the.host', 71 | port: 3130, 72 | protocol: 'http:' 73 | }; 74 | var options = ddp._getOptions(); 75 | expect(options.host).toEqual('the.host'); 76 | expect(options.port).toEqual(3130); 77 | }); 78 | it('sets the ssl to false when the protocol is http', function () { 79 | var ddp = new DDP(); 80 | ddp.url = { 81 | hostname: 'the.host', 82 | port: 3130, 83 | protocol: 'http:' 84 | }; 85 | var options = ddp._getOptions(); 86 | expect(options.ssl).toEqual(false); 87 | }); 88 | it('sets the ssl to true when the protocol is https', function () { 89 | var ddp = new DDP(); 90 | ddp.url = { 91 | hostname: 'the.host', 92 | port: 3130, 93 | protocol: 'https:' 94 | }; 95 | var options = ddp._getOptions(); 96 | expect(options.ssl).toEqual(true); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/__tests__/options-loader.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/options-loader'); 2 | jest.dontMock('path'); 3 | 4 | describe('Options Loader', () => { 5 | const optionsLoader = require('../lib/options-loader'); 6 | 7 | describe('getOptions', () => { 8 | const fs = require('fs'); 9 | 10 | beforeEach(() => { 11 | spyOn(process, 'cwd').and.returnValue('/myFolder'); 12 | optionsLoader._exit = jest.fn(); 13 | optionsLoader._requireFile = jest.fn(); 14 | }); 15 | 16 | it('should use the first argument as the config file if it is a js file containing "chimp"', () => { 17 | const argv = ['not', 'applicable', 'my-chimp-file.js']; 18 | optionsLoader._getProcessArgv = jest.fn().mockReturnValue(argv); 19 | 20 | fs.existsSync = jest.fn().mockReturnValue(true); 21 | optionsLoader.getOptions(); 22 | 23 | expect(optionsLoader._requireFile.mock.calls[0][0]).toBe('/myFolder/my-chimp-file.js'); 24 | }); 25 | 26 | it('should remove the config file from the arguments', () => { 27 | fs.existsSync = jest.fn().mockReturnValue(true); 28 | const argv = ['not', 'applicable', 'my-chimp-file.js', '2nd-arg']; 29 | optionsLoader._getProcessArgv = jest.fn().mockReturnValue(argv); 30 | 31 | optionsLoader.getOptions(); 32 | 33 | expect(argv).toEqual(['not', 'applicable', '2nd-arg']); 34 | }); 35 | 36 | it('it should log an error if the provided config file does not exist and exist with a code 1', () => { 37 | const argv = ['not', 'applicable', 'my-chimp-file.js']; 38 | optionsLoader._getProcessArgv = jest.fn().mockReturnValue(argv); 39 | 40 | fs.existsSync = jest.fn().mockReturnValue(false); 41 | optionsLoader.getOptions(); 42 | 43 | expect(optionsLoader._exit.mock.calls[0][0]).toBe(1); 44 | }); 45 | 46 | it('should load the chimp.js file if it exists in the current directory', () => { 47 | const argv = ['not', 'applicable', 'not-a-config-file.js']; 48 | optionsLoader._getProcessArgv = jest.fn().mockReturnValue(argv); 49 | 50 | fs.existsSync = jest.fn().mockReturnValue(true); 51 | optionsLoader.getOptions(); 52 | 53 | expect(optionsLoader._requireFile.mock.calls[0][0]).toBe('/myFolder/chimp.js'); 54 | }); 55 | 56 | it('should load default values from default.js', () => { 57 | const argv = ['not', 'applicable', 'not-a-config-file.js']; 58 | optionsLoader._getProcessArgv = jest.fn().mockReturnValue(argv); 59 | optionsLoader._getDefaultConfigFilePath = jest.fn().mockReturnValue('defaultFileLocation'); 60 | 61 | fs.existsSync = jest.fn().mockReturnValue(true); 62 | optionsLoader.getOptions(); 63 | 64 | expect(optionsLoader._requireFile.mock.calls[1][0]).toBe('defaultFileLocation'); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/__tests__/phantom-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/phantom'); 2 | 3 | describe('Phantom', function () { 4 | 5 | describe('constructor', function () { 6 | 7 | var Phantom = require('../lib/phantom'); 8 | 9 | it('throws when options is not passed', function () { 10 | 11 | var createPhantom = function () { 12 | new Phantom(); 13 | }; 14 | 15 | expect(createPhantom).toThrowError('options is required'); 16 | }); 17 | 18 | it('throws when options.port is not passed', function () { 19 | 20 | var options = {}; 21 | var createPhantom = function () { 22 | new Phantom(options); 23 | }; 24 | 25 | expect(createPhantom).toThrowError('options.port is required'); 26 | }); 27 | 28 | }); 29 | 30 | describe('start', function () { 31 | 32 | it('uses options.port to start phantom in webdriver mode', function () { 33 | var processHelper = require('../lib/process-helper'); 34 | var Phantom = require('../lib/phantom'); 35 | 36 | var phantom = new Phantom({port: 9876}); 37 | processHelper.start = jest.fn(); 38 | phantom.start(); 39 | 40 | expect(processHelper.start.mock.calls.length).toBe(1); 41 | expect(processHelper.start.mock.calls[0][0].args).toEqual(['--webdriver', 9876, '--ignore-ssl-errors', 'false']); 42 | }); 43 | 44 | it('sets this.child to the phantom child process', function () { 45 | 46 | var processHelper = require('../lib/process-helper'); 47 | var Phantom = require('../lib/phantom'); 48 | 49 | var childProcess = {}; 50 | processHelper.start = jest.fn(); 51 | processHelper.start.mockReturnValue(childProcess); 52 | 53 | var phantom = new Phantom({port: 9876}); 54 | expect(phantom.child).toBe(null); 55 | 56 | phantom.start(null); 57 | 58 | expect(phantom.child).toBe(childProcess); 59 | }); 60 | 61 | it('calls the callback with null when phantom starts successfully', function () { 62 | 63 | var processHelper = require('../lib/process-helper'); 64 | var Phantom = require('../lib/phantom'); 65 | 66 | processHelper.start = jest.fn().mockImplementation(function (options, callback) { 67 | callback(); 68 | }); 69 | 70 | var phantom = new Phantom({port: 9876}); 71 | var callback = jest.fn(); 72 | 73 | phantom.start(callback); 74 | 75 | expect(callback.mock.calls.length).toBe(1); 76 | expect(callback.mock.calls[0][0]).toBeFalsy(); 77 | 78 | }); 79 | 80 | it('calls the callback with the error when phantom fails to start', function () { 81 | 82 | var processHelper = require('../lib/process-helper'); 83 | var Phantom = require('../lib/phantom'); 84 | 85 | processHelper.start = jest.fn(function (options, callback) { 86 | callback('error!'); 87 | }); 88 | 89 | var phantom = new Phantom({port: 9876}); 90 | var callback = jest.fn(); 91 | 92 | phantom.start(callback); 93 | 94 | expect(callback.mock.calls.length).toBe(1); 95 | expect(callback.mock.calls[0][0]).toBe('error!'); 96 | }); 97 | 98 | it('immediately calls the callback without starting phantom if already running', function () { 99 | 100 | var processHelper = require('../lib/process-helper'); 101 | var Phantom = require('../lib/phantom'); 102 | 103 | var phantom = new Phantom({port: 9876}); 104 | phantom.child = 'not null'; 105 | 106 | var callback = jest.fn(); 107 | phantom.start(callback); 108 | processHelper.start = jest.fn(function (options, callback) { 109 | callback('error!'); 110 | }); 111 | 112 | expect(processHelper.start.mock.calls.length).toBe(0); 113 | expect(callback.mock.calls.length).toBe(1); 114 | expect(callback.mock.calls[0][0]).toBeFalsy(); 115 | expect(phantom.child).toBe('not null'); 116 | 117 | }); 118 | 119 | }); 120 | 121 | describe('stop', function () { 122 | 123 | it('kills the phantom child when phantom is running and sets the child to null', function () { 124 | 125 | var processHelper = require('../lib/process-helper'); 126 | var Phantom = require('../lib/phantom'); 127 | 128 | var phantom = new Phantom({port: 9876}); 129 | phantom.child = 'not null'; 130 | 131 | processHelper.kill = jest.fn().mockImplementation(function (options, callback) { 132 | callback(); 133 | }); 134 | 135 | var callback = jest.fn(); 136 | phantom.stop(callback); 137 | 138 | expect(processHelper.kill.mock.calls.length).toBe(1); 139 | expect(callback.mock.calls.length).toBe(1); 140 | expect(callback.mock.calls[0][0]).toBeFalsy(); 141 | expect(phantom.child).toBe(null); 142 | 143 | }); 144 | 145 | it('calls the callback immediately when phantom is not running', function () { 146 | 147 | var processHelper = require('../lib/process-helper'); 148 | var Phantom = require('../lib/phantom'); 149 | 150 | var phantom = new Phantom({port: 9876}); 151 | phantom.child = null; 152 | 153 | processHelper.kill = jest.fn(); 154 | 155 | var callback = jest.fn(); 156 | phantom.stop(callback); 157 | 158 | expect(processHelper.kill.mock.calls.length).toBe(0); 159 | expect(callback.mock.calls.length).toBe(1); 160 | expect(callback.mock.calls[0][0]).toBeFalsy(); 161 | expect(phantom.child).toBe(null); 162 | 163 | }); 164 | 165 | it('calls the callback with an error if an error is encountered', function () { 166 | var processHelper = require('../lib/process-helper'); 167 | var Phantom = require('../lib/phantom'); 168 | 169 | var phantom = new Phantom({port: 9876}); 170 | phantom.child = 'not null'; 171 | 172 | processHelper.kill = jest.fn().mockImplementation(function (options, callback) { 173 | callback('Error!'); 174 | }); 175 | 176 | var callback = jest.fn(); 177 | phantom.stop(callback); 178 | 179 | expect(processHelper.kill.mock.calls.length).toBe(1); 180 | expect(callback.mock.calls.length).toBe(1); 181 | expect(callback.mock.calls[0][0]).toBe('Error!'); 182 | }); 183 | 184 | }); 185 | 186 | describe('interrupt', function () { 187 | 188 | it('should stop phantom', function () { 189 | 190 | var Phantom = require('../lib/phantom'); 191 | 192 | var phantom = new Phantom({port: 9876}); 193 | 194 | phantom.stop = jest.fn(); 195 | 196 | var callback = 'callback'; 197 | phantom.interrupt(callback); 198 | 199 | expect(phantom.stop.mock.calls.length).toBe(1); 200 | expect(phantom.stop.mock.calls[0][0]).toBe(callback); 201 | 202 | }); 203 | 204 | }); 205 | 206 | }); 207 | -------------------------------------------------------------------------------- /src/__tests__/process-helper-spec.js: -------------------------------------------------------------------------------- 1 | // jest.dontMock('../lib/process-helper'); 2 | 3 | 4 | describe('process-helper', () => { 5 | describe('start', () => { 6 | it('spawns a child, calls the callback and returns the child', () => { 7 | const processHelper = require('../lib/process-helper.js'); 8 | 9 | const child = {}; 10 | const mockedSpawn = jest.spyOn(processHelper, "spawn"); 11 | mockedSpawn.mockReturnValue(child); 12 | 13 | const options = {}; 14 | const callback = jest.fn(); 15 | const ret = processHelper.start(options, callback); 16 | 17 | expect(ret).toBe(child); 18 | expect(callback.mock.calls.length).toBe(1); 19 | expect(callback.mock.calls[0][0]).toBeFalsy(); 20 | mockedSpawn.mockRestore(); 21 | }); 22 | 23 | it('waits for message if waitForMessage is provided and delegates the callback as is', () => { 24 | const processHelper = require('../lib/process-helper.js'); 25 | const cp = require('child_process'); 26 | 27 | const child = {}; 28 | cp.spawn = jest.fn().mockReturnValue(child) 29 | const mockedSpawn = jest.spyOn(processHelper, "spawn"); 30 | mockedSpawn.mockImplementation(() => {}); 31 | 32 | const mockedWaitForMessage = jest.spyOn(processHelper, "waitForMessage"); 33 | mockedWaitForMessage.mockImplementation(function (options, child, callback) { 34 | callback.apply(this, [1, 2, 3, 4]); 35 | }); 36 | 37 | const options = {waitForMessage: 'not null'}; 38 | const callback = jest.fn(); 39 | const ret = processHelper.start(options, callback); 40 | 41 | expect(processHelper.waitForMessage.mock.calls.length).toBe(1); 42 | expect(callback.mock.calls.length).toBe(1); 43 | expect(callback.mock.calls[0]).toEqual([1, 2, 3, 4]); 44 | mockedSpawn.mockRestore(); 45 | mockedWaitForMessage.mockRestore(); 46 | }); 47 | }); 48 | 49 | describe('spawn', () => { 50 | it('calls spawn with the binary and args and returns the child process', () => { 51 | const cp = require('child_process'); 52 | const processHelper = require('../lib/process-helper.js'); 53 | 54 | processHelper.logOutputs = jest.fn(); 55 | 56 | const child = {}; 57 | cp.spawn = jest.fn().mockReturnValue(child) 58 | 59 | const options = { 60 | bin: '/someBinary', 61 | args: ['bunch', 'of', 'args'], 62 | }; 63 | const ret = processHelper.spawn(options); 64 | 65 | expect(cp.spawn.mock.calls.length).toBe(1); 66 | expect(cp.spawn.mock.calls[0][0]).toBe(options.bin); 67 | expect(cp.spawn.mock.calls[0][1]).toBe(options.args); 68 | expect(ret).toBe(child); 69 | }); 70 | 71 | it('logs the outputs of the child process', () => { 72 | let cp = require('child_process'), 73 | processHelper = require('../lib/process-helper.js'); 74 | 75 | processHelper.logOutputs = jest.fn(); 76 | 77 | const child = {}; 78 | spyOn(cp, 'spawn').and.returnValue(child); 79 | 80 | const options = { 81 | prefix: 'hey bear', 82 | }; 83 | const ret = processHelper.spawn(options); 84 | 85 | expect(processHelper.logOutputs.mock.calls.length).toBe(1); 86 | expect(processHelper.logOutputs.mock.calls[0][0]).toBe(options.prefix); 87 | expect(processHelper.logOutputs.mock.calls[0][1]).toBe(child); 88 | }); 89 | }); 90 | 91 | describe('logOutputs', () => { 92 | it('logs the output of the child process stderr events', () => { 93 | let log = require('../lib/log.js'), 94 | processHelper = require('../lib/process-helper.js'); 95 | 96 | log.debug = jest.fn(); 97 | const child = { 98 | stdout: { 99 | on: jest.fn().mockImplementation((event, eventTrigger) => { 100 | eventTrigger('blah'); 101 | expect(event).toBe('data'); 102 | expect(log.debug.mock.calls.length).toBe(1); 103 | expect(log.debug.mock.calls[0][0]).toBe('[chimp][prefix.stdout]'); 104 | expect(log.debug.mock.calls[0][1]).toBe('blah'); 105 | }), 106 | }, 107 | stderr: { 108 | on: jest.fn().mockImplementation((event, eventTrigger) => { 109 | eventTrigger('blah blah'); 110 | expect(event).toBe('data'); 111 | expect(log.debug.mock.calls.length).toBe(2); 112 | expect(log.debug.mock.calls[1][0]).toBe('[chimp][prefix.stderr]'); 113 | expect(log.debug.mock.calls[1][1]).toBe('blah blah'); 114 | }), 115 | }, 116 | }; 117 | 118 | processHelper.logOutputs('prefix', child); 119 | }); 120 | }); 121 | 122 | describe('waitForMessage', () => { 123 | it('removes the listener if the success message is seen and calls the callback', () => { 124 | const processHelper = require('../lib/process-helper.js'); 125 | 126 | const callback = jest.fn(); 127 | 128 | const options = { 129 | prefix: '[apollo]', 130 | waitForMessage: 'we have lift off', 131 | }; 132 | 133 | let eventToBeRemovedStdOut = false; 134 | let eventToBeRemovedStdErr = false; 135 | const child = { 136 | stdout: { 137 | on: jest.fn().mockImplementation((event, eventTrigger) => { 138 | eventToBeRemovedStdOut = eventTrigger; 139 | eventTrigger('Huston, we have lift off!'); 140 | }), 141 | removeListener: jest.fn(), 142 | }, 143 | stderr: { 144 | on: jest.fn().mockImplementation((event, eventTrigger) => { 145 | eventToBeRemovedStdErr = eventTrigger; 146 | eventTrigger('Huston, we have lift off!'); 147 | }), 148 | removeListener: jest.fn(), 149 | }, 150 | }; 151 | 152 | processHelper.waitForMessage(options, child, callback); 153 | 154 | expect(child.stdout.removeListener.mock.calls.length).toBe(1); 155 | expect(child.stdout.removeListener.mock.calls[0][0]).toBe('data'); 156 | expect(child.stdout.removeListener.mock.calls[0][1]).toBe(eventToBeRemovedStdOut); 157 | 158 | expect(child.stderr.removeListener.mock.calls.length).toBe(1); 159 | expect(child.stderr.removeListener.mock.calls[0][0]).toBe('data'); 160 | expect(child.stderr.removeListener.mock.calls[0][1]).toBe(eventToBeRemovedStdErr); 161 | 162 | expect(callback.mock.calls.length).toBe(2); 163 | expect(callback.mock.calls[0][0]).toBeFalsy(); 164 | expect(callback.mock.calls[1][0]).toBeFalsy(); 165 | }); 166 | 167 | it('calls back with an error if the error message is seen', () => { 168 | const processHelper = require('../lib/process-helper.js'); 169 | 170 | const callback = jest.fn(); 171 | 172 | const options = { 173 | prefix: '[apollo]', 174 | waitForMessage: 'not empty', 175 | errorMessage: 'engine failure', 176 | }; 177 | 178 | const eventToBeRemoved = false; 179 | const child = { 180 | stdout: { 181 | on: jest.fn().mockImplementation((event, eventTrigger) => { 182 | eventTrigger('Huston, we have a problem - engine failure!'); 183 | }), 184 | }, 185 | stderr: { 186 | on: jest.fn().mockImplementation((event, eventTrigger) => { 187 | eventTrigger('Huston, we have a problem - engine failure!'); 188 | }), 189 | }, 190 | }; 191 | 192 | processHelper.waitForMessage(options, child, callback); 193 | 194 | expect(callback.mock.calls.length).toBe(2); 195 | expect(callback.mock.calls[0][0]).toBe('Huston, we have a problem - engine failure!'); 196 | expect(callback.mock.calls[1][0]).toBe('Huston, we have a problem - engine failure!'); 197 | }); 198 | }); 199 | 200 | describe('kill', () => { 201 | it('kills the provided process, sets it to null and calls the callback when the process is dead', () => { 202 | const processHelper = require('../lib/process-helper.js'); 203 | 204 | process.kill = jest.fn().mockImplementation(() => { 205 | // the first call checks if the process exists 206 | // the second call is the actual kill 207 | // subsequent calls are checking if the process exists 208 | // it takes 3 calls to go through all the execution paths for this SUT 209 | if (process.kill.mock.calls.length === 4) { 210 | throw ({code: 'ESRCH'}); 211 | } 212 | }); 213 | 214 | const options = { 215 | child: { 216 | pid: 1234, 217 | }, 218 | }; 219 | const callback = jest.fn(); 220 | jest.useFakeTimers(); 221 | processHelper.kill(options, callback); 222 | jest.runAllTimers(); 223 | 224 | expect(process.kill.mock.calls.length).toBe(4); 225 | expect(process.kill.mock.calls[0][0]).toEqual(1234); 226 | expect(process.kill.mock.calls[0][1]).toBe(0); 227 | expect(process.kill.mock.calls[1][1]).toBe('SIGTERM'); 228 | expect(process.kill.mock.calls[2][0]).toEqual(1234); 229 | expect(process.kill.mock.calls[2][1]).toEqual(0); 230 | expect(process.kill.mock.calls[3][0]).toEqual(1234); 231 | expect(process.kill.mock.calls[3][1]).toEqual(0); 232 | 233 | expect(options.child).toBe(null); 234 | 235 | expect(callback.mock.calls.length).toBe(1); 236 | expect(callback.mock.calls[0][0]).toBeFalsy(); 237 | }); 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /src/__tests__/saucelabs-manager-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/saucelabs-manager'); 2 | 3 | describe('SauceLabs Session Manager', function () { 4 | 5 | describe('Constructor', function () { 6 | 7 | it('sets the SauceLabsUrl', function () { 8 | var SauceLabsManager = require('../lib/saucelabs-manager'); 9 | var options = {port: 1234, browser: 'something', user: 'testus3r', key: '12345678', host: 'saucelabs.com'}; 10 | 11 | var sauceLabsBaseUrl = 'https://' + options.user + ':' + options.key + '@' + options.host + '/rest/v1/' + options.user; 12 | 13 | var session = new SauceLabsManager(options); 14 | 15 | expect(session.options.sauceLabsUrl).toBe(sauceLabsBaseUrl); 16 | }); 17 | 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/session-factory-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/session-factory'); 2 | jest.dontMock('../lib/session-manager'); 3 | 4 | describe('Session Factory', function () { 5 | 6 | describe('Constructor', function () { 7 | 8 | it('sets the options on the instance when no exceptions are thrown', function () { 9 | var SessionFactory = require('../lib/session-factory'); 10 | var options = {port: 1234, browser: 'something'}; 11 | 12 | var session = new SessionFactory(options); 13 | 14 | expect(session.options).toBe(options); 15 | }); 16 | 17 | it('throws when options is not passed', function () { 18 | var SessionFactory = require('../lib/session-factory'); 19 | var session = function () { 20 | new SessionFactory(); 21 | }; 22 | expect(session).toThrowError('options is required'); 23 | }); 24 | 25 | it('throws when options.port is not passed', function () { 26 | var SessionFactory = require('../lib/session-factory'); 27 | var options = {}; 28 | var session = function () { 29 | new SessionFactory({}); 30 | }; 31 | expect(session).toThrowError('options.port is required'); 32 | }); 33 | 34 | it('throws when options.browser and options.device is not passed', function () { 35 | var SessionFactory = require('../lib/session-factory'); 36 | var options = {port: 1234}; 37 | var session = function () { 38 | new SessionFactory(options); 39 | }; 40 | expect(session).toThrowError('[chimp][session-manager-factory] options.browser or options.deviceName is required'); 41 | }); 42 | 43 | it('throws when options.user and options.key is not passed and not using localhost', function () { 44 | var SessionFactory = require('../lib/session-factory'); 45 | var options = {port: 1234, browser: 'firefox', host: 'browserstack.com'}; 46 | var session = function () { 47 | new SessionFactory(options); 48 | }; 49 | expect(session).toThrowError('[chimp][session-manager-factory] options.user and options.key are required'); 50 | }); 51 | 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /src/__tests__/session-manager-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('underscore'); 2 | jest.dontMock('../lib/session-manager'); 3 | jest.dontMock('../lib/boolean-helper'); 4 | 5 | //jest.dontMock('loglevel'); 6 | //require('loglevel').setLevel('TRACE'); 7 | 8 | describe('Session Manager', function () { 9 | 10 | describe('Constructor', function () { 11 | 12 | it('sets the options on the instance when no exceptions are thrown', function () { 13 | var SessionManager = require('../lib/session-manager'); 14 | var options = {port: 1234, browser: 'something'}; 15 | 16 | var sessionManager = new SessionManager(options); 17 | 18 | expect(sessionManager.options).toBe(options); 19 | }); 20 | 21 | }); 22 | 23 | describe('Remote', function () { 24 | 25 | beforeEach(function () { 26 | delete process.env['chimp.noSessionReuse']; 27 | delete process.env['chimp.watch']; 28 | }); 29 | 30 | it('should delegate the webdriver remote call if using phantom', function () { 31 | 32 | var wd = require('xolvio-sync-webdriverio'); 33 | var SessionManager = require('../lib/session-manager'); 34 | 35 | wd.remote = jest.fn(). 36 | mockReturnValueOnce('return from remote1'). 37 | mockReturnValueOnce('return from remote2'); 38 | var sessionManager = new SessionManager({port: 1234, browser: 'phantomjs'}); 39 | 40 | var sessions = [1234]; 41 | sessionManager._getWebdriverSessions = jest.fn().mockImplementation(function (callback) { 42 | callback(sessions); 43 | }); 44 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 45 | callback(); 46 | }); 47 | 48 | var options = {some: 'options'}; 49 | 50 | var callback = jest.fn(); 51 | sessionManager.remote(options, callback); 52 | sessionManager.remote(options, callback); 53 | 54 | 55 | expect(wd.remote.mock.calls.length).toBe(2); 56 | expect(callback.mock.calls[0][1]).toBe('return from remote1'); 57 | expect(callback.mock.calls[1][1]).toBe('return from remote2'); 58 | expect(wd.remote.mock.calls[0][0]).toBe(options); 59 | expect(wd.remote.mock.calls[1][0]).toBe(options); 60 | 61 | }); 62 | 63 | it('should delegate the webdriver remote call if a session has not already started', function () { 64 | 65 | var wd = require('xolvio-sync-webdriverio'); 66 | var SessionManager = require('../lib/session-manager'); 67 | 68 | var browser = {}; 69 | wd.remote = jest.fn().mockReturnValue(browser); 70 | 71 | var sessionManager = new SessionManager({port: 1234, browser: 'something'}); 72 | 73 | var sessions = []; 74 | sessionManager._getWebdriverSessions = jest.fn().mockImplementation(function (callback) { 75 | callback(null, sessions); 76 | }); 77 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 78 | callback(); 79 | }); 80 | 81 | var options = {some: 'options'}; 82 | var callback = jest.fn(); 83 | sessionManager.remote(options, callback); 84 | 85 | expect(callback.mock.calls[0][1]).toBe(browser); 86 | expect(wd.remote.mock.calls.length).toBe(1); 87 | expect(wd.remote.mock.calls[0][0]).toBe(options); 88 | 89 | }); 90 | 91 | it('should reuse a session if one has already started in watch mode', function () { 92 | 93 | var wd = require('xolvio-sync-webdriverio'); 94 | var SessionManager = require('../lib/session-manager'); 95 | 96 | process.env['chimp.watch'] = true; 97 | var browser = {_original: {requestHandler: {sessionID: 'some-id'}}}; 98 | wd.remote = jest.fn().mockReturnValue(browser); 99 | 100 | var sessionManager = new SessionManager({port: 1234, browser: 'something'}); 101 | 102 | var sessions = [{id: 'session-id'}]; 103 | sessionManager._getWebdriverSessions = jest.fn().mockImplementation(function (callback) { 104 | callback(null, sessions); 105 | }); 106 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 107 | callback(); 108 | }); 109 | 110 | var options = {some: 'options'}; 111 | var callback = jest.fn(); 112 | sessionManager.remote(options, callback); 113 | 114 | 115 | expect(callback.mock.calls[0][1]).toBe(browser); 116 | expect(browser._original.requestHandler.sessionID).toBe(sessions[0].id); 117 | 118 | expect(wd.remote.mock.calls.length).toBe(1); 119 | expect(wd.remote.mock.calls[0][0]).toBe(options); 120 | 121 | }); 122 | 123 | it('should reuse a session if one has already started in server mode', function () { 124 | 125 | var wd = require('xolvio-sync-webdriverio'); 126 | var SessionManager = require('../lib/session-manager'); 127 | 128 | process.env['chimp.server'] = true; 129 | var browser = {_original: {requestHandler: {sessionID: 'some-id'}}}; 130 | wd.remote = jest.fn().mockReturnValue(browser); 131 | 132 | var sessionManager = new SessionManager({port: 1234, browser: 'something'}); 133 | 134 | var sessions = [{id: 'session-id'}]; 135 | sessionManager._getWebdriverSessions = jest.fn().mockImplementation(function (callback) { 136 | callback(null, sessions); 137 | }); 138 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 139 | callback(); 140 | }); 141 | 142 | var options = {some: 'options'}; 143 | var callback = jest.fn(); 144 | sessionManager.remote(options, callback); 145 | 146 | 147 | expect(callback.mock.calls[0][1]).toBe(browser); 148 | expect(browser._original.requestHandler.sessionID).toBe(sessions[0].id); 149 | 150 | expect(wd.remote.mock.calls.length).toBe(1); 151 | expect(wd.remote.mock.calls[0][0]).toBe(options); 152 | 153 | }); 154 | 155 | it('starts a new session when noSessionReuse is true when a session exists', function () { 156 | 157 | var wd = require('xolvio-sync-webdriverio'); 158 | var SessionManager = require('../lib/session-manager'); 159 | 160 | wd.remote = jest.fn(). 161 | mockReturnValueOnce('return from remote1'). 162 | mockReturnValueOnce('return from remote2'); 163 | var sessionManager = new SessionManager({port: 1234, browser: 'something'}); 164 | 165 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 166 | callback(); 167 | }); 168 | 169 | var options = {some: 'options'}; 170 | process.env['chimp.noSessionReuse'] = true; 171 | var callback = jest.fn(); 172 | sessionManager.remote(options, callback); 173 | sessionManager.remote(options, callback); 174 | 175 | expect(callback.mock.calls[0][1]).toBe('return from remote1'); 176 | expect(callback.mock.calls[1][1]).toBe('return from remote2'); 177 | expect(callback.mock.calls[1][1].requestHandler).toBeFalsy(); 178 | expect(callback.mock.calls[1][1].requestHandler).toBeFalsy(); 179 | 180 | expect(wd.remote.mock.calls.length).toBe(2); 181 | expect(wd.remote.mock.calls[0][0]).toBe(options); 182 | expect(wd.remote.mock.calls[1][0]).toBe(options); 183 | 184 | }); 185 | 186 | it('respects noSessionReuse in watch mode', function () { 187 | 188 | var wd = require('xolvio-sync-webdriverio'); 189 | var SessionManager = require('../lib/session-manager'); 190 | 191 | wd.remote = jest.fn(). 192 | mockReturnValueOnce('return from remote1'). 193 | mockReturnValueOnce('return from remote2'); 194 | var sessionManager = new SessionManager({port: 1234, browser: 'something'}); 195 | 196 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 197 | callback(); 198 | }); 199 | 200 | var options = {some: 'options'}; 201 | process.env['chimp.watch'] = true; 202 | process.env['chimp.noSessionReuse'] = true; 203 | var callback = jest.fn(); 204 | sessionManager.remote(options, callback); 205 | sessionManager.remote(options, callback); 206 | 207 | expect(callback.mock.calls[0][1]).toBe('return from remote1'); 208 | expect(callback.mock.calls[1][1]).toBe('return from remote2'); 209 | expect(callback.mock.calls[0][1].requestHandler).toBeFalsy(); 210 | expect(callback.mock.calls[1][1].requestHandler).toBeFalsy(); 211 | 212 | expect(wd.remote.mock.calls.length).toBe(2); 213 | expect(wd.remote.mock.calls[0][0]).toBe(options); 214 | expect(wd.remote.mock.calls[1][0]).toBe(options); 215 | 216 | }); 217 | 218 | it('should monkey patch the browser when reusing sessions in watch mode', function () { 219 | 220 | var wd = require('xolvio-sync-webdriverio'); 221 | var SessionManager = require('../lib/session-manager'); 222 | var sessionManager = new SessionManager({port: 1234, browser: 'somebrowser'}); 223 | var sessions = []; 224 | sessionManager._getWebdriverSessions = jest.fn().mockImplementation(function (callback) { 225 | callback(null, sessions); 226 | }); 227 | sessionManager._waitForConnection = jest.fn().mockImplementation(function (browser, callback) { 228 | callback(); 229 | }); 230 | 231 | wd.remote = jest.fn(); 232 | process.env['chimp.watch'] = true; 233 | sessionManager._monkeyPatchBrowserSessionManagement = jest.fn(); 234 | 235 | var options = {some: 'options'}; 236 | var callback = jest.fn(); 237 | sessionManager.remote(options, callback); 238 | 239 | expect(sessionManager._monkeyPatchBrowserSessionManagement.mock.calls.length).toBe(1); 240 | }); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /src/__tests__/simian-reporter-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/simian-reporter'); 2 | 3 | describe('Simian reporter', function () { 4 | 5 | it('sends the results to Simian and adds the access token', function () { 6 | 7 | var request = require('request'); 8 | 9 | var SimianReporter = require('../lib/simian-reporter'); 10 | var simianReporter = new SimianReporter({ 11 | simianResultEndPoint: 'api.simian.io/v1.0/result', 12 | simianAccessToken: 'secretToken' 13 | }); 14 | 15 | var callback = jest.fn(); 16 | var result = [{id: 'Use-browser-inside-steps'}]; 17 | simianReporter.report(result, callback); 18 | 19 | expect(request.post.mock.calls.length).toBe(1); 20 | 21 | var postedObject = request.post.mock.calls[0][0]; 22 | 23 | expect(postedObject.url).toEqual('http://api.simian.io/v1.0/result?accessToken=secretToken'); 24 | expect(postedObject.json).toEqual(true); 25 | expect(postedObject.body).toEqual({ 26 | type: 'cucumber', 27 | result: result 28 | }); 29 | 30 | 31 | // calls back on a good response 32 | var postCallback = request.post.mock.calls[0][1]; 33 | postCallback(null, {statusCode: 200}); 34 | expect(callback.mock.calls.length).toBe(1); 35 | 36 | }); 37 | 38 | it('shows the error to the user when Simian returns a non 200 response', function () { 39 | 40 | var request = require('request'); 41 | 42 | var SimianReporter = require('../lib/simian-reporter'); 43 | var simianReporter = new SimianReporter({ 44 | simianAccessToken: 'secretToken' 45 | }); 46 | 47 | const log = require('../lib/log'); 48 | spyOn(log, 'error'); 49 | 50 | var callback = jest.fn(); 51 | var result = [{id: 'Use-browser-inside-steps'}]; 52 | simianReporter.report(result, callback); 53 | 54 | 55 | // calls back on a good response 56 | var postCallback = request.post.mock.calls[0][1]; 57 | var response = { 58 | statusCode: 401 59 | }; 60 | var body = { 61 | status: 'error', 62 | error: 'invalid accessToken' 63 | }; 64 | postCallback(null, response, body); 65 | expect(callback.mock.calls.length).toBe(1); 66 | expect(log.error).toHaveBeenCalledWith('[chimp][simian-reporter] Error from Simian:', 'invalid accessToken'); 67 | }); 68 | 69 | it('shows the error to the user when a request error happens before reaching the Simian API', function () { 70 | 71 | var request = require('request'); 72 | 73 | var SimianReporter = require('../lib/simian-reporter'); 74 | var simianReporter = new SimianReporter({ 75 | simianAccessToken: 'secretToken' 76 | }); 77 | 78 | const log = require('../lib/log'); 79 | spyOn(log, 'error'); 80 | 81 | var callback = jest.fn(); 82 | var result = [{id: 'Use-browser-inside-steps'}]; 83 | simianReporter.report(result, callback); 84 | 85 | 86 | // calls back on a good response 87 | var postCallback = request.post.mock.calls[0][1]; 88 | var error = new Error('network error'); 89 | var response = null; 90 | var body = null; 91 | postCallback(error, response, body); 92 | expect(callback.mock.calls.length).toBe(1); 93 | expect(log.error).toHaveBeenCalledWith( 94 | '[chimp][simian-reporter]', 'Error while sending result to Simian:', error 95 | ); 96 | }); 97 | 98 | }); 99 | 100 | -------------------------------------------------------------------------------- /src/__tests__/testingbot-manager-spec.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../lib/testingbot-manager'); 2 | 3 | describe('TestingBot Session Manager', function () { 4 | 5 | describe('Constructor', function () { 6 | 7 | it('sets the TestingBotUrl', function () { 8 | var TestingBotManager = require('../lib/testingbot-manager'); 9 | var options = {port: 1234, browser: 'something', user: 'testus3r', key: '12345678', host: 'api.testingbot.com'}; 10 | 11 | var testingBotBaseUrl = 'https://' + options.user + ':' + options.key + '@' + options.host + '/v1/'; 12 | 13 | var session = new TestingBotManager(options); 14 | 15 | expect(session.options.testingbotBaseUrl).toBe(testingBotBaseUrl); 16 | }); 17 | 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /src/bin/chimp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Chimp = require('../lib/chimp.js'), 4 | minimist = require('minimist'), 5 | freeport = require('freeport'), 6 | exit = require('exit'), 7 | log = require('../lib/log'), 8 | fs = require('fs'), 9 | _ = require('underscore'), 10 | path = require('path'), 11 | optionsLoader = require('../lib/options-loader'), 12 | packageJson = require('../../package.json'); 13 | 14 | // Make babel plugins available to Cucumber and Mocha child processes 15 | process.env.NODE_PATH += path.delimiter + path.resolve(__dirname, '../../node_modules') + 16 | path.delimiter + path.resolve(__dirname, '../../../../node_modules'); 17 | 18 | var argv = minimist(process.argv, { 19 | default: optionsLoader.getOptions(), 20 | boolean: [ 21 | // - - - - CHIMP - - - - 22 | 'watch', 23 | 'watchWithPolling', 24 | 'server', 25 | 'sync', 26 | 'offline', 27 | 28 | // - - - - CUCUMBER - - - - 29 | 'singleSnippetPerFile', 30 | 'chai', 31 | 'screenshotsOnError', 32 | 'captureAllStepScreenshots', 33 | 'saveScreenshotsToDisk', 34 | 'saveScreenshotsToReport', 35 | 36 | // - - - - SELENIUM - - - - 37 | 38 | // - - - - WEBDRIVER-IO - - - - 39 | 40 | // - - - - SESSION-MANAGER - - - - 41 | 'noSessionReuse', 42 | 43 | // - - - - SIMIAN - - - - 44 | 45 | // - - - - MOCHA - - - - 46 | 'mocha', 47 | 48 | // - - - - METEOR - - - - 49 | 50 | // - - - - DEBUGGING - - - - 51 | 'debug', 52 | ], 53 | }); 54 | 55 | if (argv.host && ((argv.host.indexOf('sauce') !== -1 || argv.host.indexOf('browserstack') !== -1) || argv.host.indexOf('testingbot') !== -1)) { 56 | argv.noSessionReuse = true; 57 | } 58 | 59 | if (argv.deviceName) { 60 | argv.browser = ''; 61 | } 62 | 63 | if (argv.v || argv.version) { 64 | console.log(packageJson.version); 65 | process.exit(); 66 | } 67 | 68 | try { 69 | if (!argv.port) { 70 | freeport(function (error, port) { 71 | if (error) { 72 | throw error; 73 | } 74 | argv.port = port; 75 | startChimp(argv); 76 | }); 77 | } else { 78 | startChimp(argv); 79 | } 80 | 81 | } catch (ex) { 82 | process.stderr.write(ex.stack + '\n'); 83 | exit(2); 84 | } 85 | 86 | function startChimp(options) { 87 | var chimp = new Chimp(options); 88 | chimp.init(function (err) { 89 | if (err) { 90 | log.error(err); 91 | log.debug('Error in chimp init', err); 92 | } 93 | exit(err ? 2 : 0); 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /src/bin/default.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {isCI} from '../lib/ci'; 3 | 4 | const chromedriver = require('chromedriver'); 5 | 6 | if (!chromedriver) { 7 | throw new Error('Please install chromedriver as a dev dependency in your project: npm install --save-dev chromedriver') 8 | } 9 | 10 | module.exports = { 11 | // - - - - CHIMP - - - - 12 | watch: false, 13 | watchTags: '@watch,@focus', 14 | domainSteps: null, 15 | e2eSteps: null, 16 | fullDomain: false, 17 | domainOnly: false, 18 | e2eTags: '@e2e', 19 | watchWithPolling: false, 20 | server: false, 21 | serverPort: 8060, 22 | serverHost: 'localhost', 23 | sync: true, 24 | offline: false, 25 | showXolvioMessages: true, 26 | 'fail-when-no-tests-run': false, 27 | 28 | // - - - - CUCUMBER - - - - 29 | path: './features', 30 | format: 'pretty', 31 | tags: '~@ignore', 32 | singleSnippetPerFile: true, 33 | recommendedFilenameSeparator: '_', 34 | chai: false, 35 | screenshotsOnError: isCI(), 36 | screenshotsPath: '.screenshots', 37 | captureAllStepScreenshots: false, 38 | saveScreenshotsToDisk: true, 39 | // Note: With a large viewport size and captureAllStepScreenshots enabled, 40 | // you may run out of memory. Use browser.setViewportSize to make the 41 | // viewport size smaller. 42 | saveScreenshotsToReport: false, 43 | jsonOutput: null, 44 | compiler: 'js:' + path.resolve(__dirname, '../lib/babel-register.js'), 45 | conditionOutput: true, 46 | 47 | // - - - - SELENIUM - - - - 48 | browser: null, 49 | platform: 'ANY', 50 | name: '', 51 | user: '', 52 | key: '', 53 | port: null, 54 | host: null, 55 | // deviceName: null, 56 | 57 | // - - - - WEBDRIVER-IO - - - - 58 | webdriverio: { 59 | desiredCapabilities: {}, 60 | logLevel: 'silent', 61 | // logOutput: null, 62 | host: '127.0.0.1', 63 | port: 4444, 64 | path: '/wd/hub', 65 | baseUrl: null, 66 | coloredLogs: true, 67 | screenshotPath: null, 68 | waitforTimeout: 500, 69 | waitforInterval: 250, 70 | }, 71 | 72 | // - - - - SELENIUM-STANDALONE 73 | seleniumStandaloneOptions: { 74 | // check for more recent versions of selenium here: 75 | // http://selenium-release.storage.googleapis.com/index.html 76 | version: '3.8.1', 77 | baseURL: 'https://selenium-release.storage.googleapis.com', 78 | drivers: { 79 | chrome: { 80 | // check for more recent versions of chrome driver here: 81 | // http://chromedriver.storage.googleapis.com/index.html 82 | version: chromedriver.version, 83 | arch: process.arch, 84 | baseURL: 'https://chromedriver.storage.googleapis.com' 85 | }, 86 | ie: { 87 | // check for more recent versions of internet explorer driver here: 88 | // http://selenium-release.storage.googleapis.com/index.html 89 | version: '3.0.0', 90 | arch: 'ia32', 91 | baseURL: 'https://selenium-release.storage.googleapis.com' 92 | }, 93 | firefox: { 94 | // check for more recent versions of gecko driver here: 95 | // https://github.com/mozilla/geckodriver/releases 96 | version: '0.19.1', 97 | arch: process.arch, 98 | baseURL: 'https://github.com/mozilla/geckodriver/releases/download' 99 | } 100 | } 101 | }, 102 | 103 | // - - - - SESSION-MANAGER - - - - 104 | noSessionReuse: false, 105 | 106 | // - - - - SIMIAN - - - - 107 | simianResultEndPoint: 'api.simian.io/v1.0/result', 108 | simianAccessToken: false, 109 | simianResultBranch: null, 110 | simianRepositoryId: null, 111 | 112 | // - - - - MOCHA - - - - 113 | mocha: false, 114 | mochaCommandLineOptions: {bail: true}, 115 | mochaConfig: { 116 | // tags and grep only work when watch mode is false 117 | tags: '', 118 | grep: null, 119 | timeout: 60000, 120 | reporter: 'spec', 121 | slow: 10000, 122 | useColors: true 123 | }, 124 | 125 | // - - - - JASMINE - - - - 126 | jasmine: false, 127 | jasmineConfig: { 128 | specDir: '.', 129 | specFiles: [ 130 | '**/*@(_spec|-spec|Spec).@(js|jsx)', 131 | ], 132 | helpers: [ 133 | 'support/**/*.@(js|jsx)', 134 | ], 135 | stopSpecOnExpectationFailure: false, 136 | random: false, 137 | }, 138 | jasmineReporterConfig: { 139 | // This options are passed to jasmine.configureDefaultReporter(...) 140 | // See: http://jasmine.github.io/2.4/node.html#section-Reporters 141 | }, 142 | 143 | // - - - - METEOR - - - - 144 | ddp: false, 145 | serverExecuteTimeout: 10000, 146 | 147 | // - - - - PHANTOM - - - - 148 | phantom_w: 1280, 149 | phantom_h: 1024, 150 | phantom_ignoreSSLErrors: false, 151 | 152 | // - - - - DEBUGGING - - - - 153 | log: 'info', 154 | debug: false, 155 | seleniumDebug: null, 156 | debugCucumber: null, 157 | debugBrkCucumber: null, 158 | debugMocha: null, 159 | debugBrkMocha: null, 160 | }; 161 | -------------------------------------------------------------------------------- /src/lib/babel-register.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | 'presets': ['@babel/env'], 3 | 'rootMode': 'upward-optional', 4 | }); 5 | 6 | require('@babel/polyfill'); 7 | -------------------------------------------------------------------------------- /src/lib/boolean-helper.js: -------------------------------------------------------------------------------- 1 | module.exports = {isFalsey, isTruthy}; 2 | 3 | function isFalsey(variable) { 4 | return !variable || variable === 'false' || variable === 'null' || variable === ''; 5 | } 6 | 7 | function isTruthy(variable) { 8 | return !isFalsey(variable); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/browserstack-manager.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | log = require('./log'), 3 | _ = require('underscore'); 4 | 5 | /** 6 | * SessionManager Constructor 7 | * 8 | * @param {Object} options 9 | * @api public 10 | */ 11 | function BrowserStackSessionManager(options) { 12 | 13 | log.debug('[chimp][browserstack-session-manager] options are', options); 14 | 15 | var host = options.host.replace(/hub\.|hub-cloud\./, 'api.'); 16 | var browserStackBaseUrl = 'https://' + options.user + ':' + options.key + '@' + host; 17 | options.browserStackUrl = browserStackBaseUrl; 18 | 19 | this.options = options; 20 | 21 | this.maxRetries = 30; 22 | this.retryDelay = 3000; 23 | this.retry = 0; 24 | 25 | // this will be set by the remote and multiremote methods from 26 | // desiredCapabilities 27 | this.buildName = null; 28 | 29 | log.debug('[chimp][browserstack-session-manager] created a new SessionManager', options); 30 | 31 | } 32 | 33 | BrowserStackSessionManager.prototype.webdriver = require('xolvio-sync-webdriverio'); 34 | 35 | /** 36 | * Wraps the webdriver remote method and allows reuse options 37 | * 38 | * @api public 39 | */ 40 | BrowserStackSessionManager.prototype.remote = function (webdriverOptions, callback) { 41 | 42 | var self = this; 43 | 44 | log.debug('[chimp][browserstack-session-manager] creating webdriver remote '); 45 | var browser = this.webdriver.remote(webdriverOptions); 46 | 47 | this.buildName = webdriverOptions.desiredCapabilities.build; 48 | log.debug('[chimp][browserstack-session-manager] BuildName: ' + this.buildName); 49 | 50 | callback(null, browser); 51 | return; 52 | }; 53 | 54 | /** 55 | * Wraps the webdriver multiremote method and allows reuse options 56 | * 57 | * @api public 58 | */ 59 | BrowserStackSessionManager.prototype.multiremote = function (webdriverOptions, callback) { 60 | log.debug('[chimp][browserstack-session-manager] creating webdriver remote '); 61 | var browser = this.webdriver.multiremote(webdriverOptions); 62 | this.buildName = webdriverOptions['browser0'].desiredCapabilities.build; 63 | log.debug('[chimp][browserstack-session-manager] BuildName: ' + this.buildName); 64 | callback(null, browser); 65 | return; 66 | }; 67 | 68 | /** 69 | * Gets a list of builds from the BrowserStack API as sessions must be queried based on Builds 70 | * 71 | * @api private 72 | */ 73 | BrowserStackSessionManager.prototype._getBuilds = function (callback) { 74 | var hub = this.options.browserStackUrl + '/automate/builds.json?status=running'; 75 | 76 | log.debug('[chimp][browserstack-session-manager]', 'requesting builds from', hub); 77 | 78 | request(hub, function (error, response, body) { 79 | if (!error && response.statusCode === 200) { 80 | log.debug('[chimp][browserstack-session-manager]', 'received data', body); 81 | callback(null, JSON.parse(body)); 82 | } else { 83 | log.error('[chimp][browserstack-session-manager]', 'received error', error); 84 | callback(error); 85 | } 86 | }); 87 | }; 88 | 89 | /** 90 | * Gets a list of sessions from the BrowserStack API based on Build ID 91 | * 92 | * @api private 93 | */ 94 | BrowserStackSessionManager.prototype._getSessions = function (buildId, callback) { 95 | var hub = this.options.browserStackUrl + '/automate/builds/' + buildId + '/sessions.json?status=running'; 96 | 97 | log.debug('[chimp][browserstack-session-manager]', 'requesting sessions from', hub); 98 | 99 | request(hub, function (error, response, body) { 100 | if (!error && response.statusCode === 200) { 101 | log.debug('[chimp][browserstack-session-manager]', 'received data', body); 102 | callback(null, JSON.parse(body)); 103 | } else { 104 | log.error('[chimp][browserstack-session-manager]', 'received error', error); 105 | callback(error); 106 | } 107 | }); 108 | }; 109 | 110 | /** 111 | * Kills all sessions in the matching running build found on 112 | * BrowserStack. 113 | * 114 | * If 'build' is specified in the desiredCapabilities, it will find a 115 | * running build with the specified build name, else it will consider 116 | * the first running build. When using multiremote, it's important to 117 | * to specify a unique 'build' in desiredCapabilities so that all and 118 | * only those sessions created by the current build are killed. 119 | * 120 | * @api public 121 | */ 122 | BrowserStackSessionManager.prototype.killCurrentSession = function (callback) { 123 | 124 | var self = this; 125 | var wdOptions = this.options; 126 | const wdHubSession = 'http://' + wdOptions.host + ':' + wdOptions.port + '/wd/hub/session'; 127 | 128 | var killSession = function (session) { 129 | request.del(wdHubSession + '/' + session.automation_session.hashed_id, function (wdError, wdResponse) { 130 | if (!wdError && wdResponse.statusCode === 200) { 131 | var options = { 132 | url: wdOptions.browserStackUrl + '/automate/sessions/' + session.automation_session.hashed_id + '.json', 133 | method: 'PUT', 134 | json: true, 135 | body: {status: 'completed'} 136 | }; 137 | request(options, function (error, response) { 138 | if (!error && response.statusCode === 200) { 139 | log.debug('[chimp][browserstack-session-manager]', 'stopped session'); 140 | callback(); 141 | } else { 142 | log.error('[chimp][browserstack-session-manager]', 'received error', error); 143 | callback(error); 144 | } 145 | }); 146 | } 147 | else { 148 | log.error('[chimp][browserstack-session-manager]', 'received error', wdError); 149 | callback(wdError); 150 | } 151 | }); 152 | }; 153 | 154 | var findBuild = function (builds) { 155 | if (self.buildName) { 156 | return _.find(builds, function (b) { 157 | return b.automation_build.name === self.buildName; 158 | }); 159 | } else { 160 | return builds[0]; 161 | } 162 | }; 163 | 164 | this._getBuilds(function (err, builds) { 165 | if (builds && builds.length) { 166 | var build = findBuild(builds); 167 | log.debug('[chimp][browserstack-session-manager]', builds, build); 168 | var buildId = build.automation_build.hashed_id; 169 | } 170 | if (buildId !== '') { 171 | self._getSessions(buildId, function (err, sessions) { 172 | if (sessions && sessions.length) { 173 | sessions.forEach(killSession); 174 | } 175 | }); 176 | } 177 | }); 178 | }; 179 | 180 | module.exports = BrowserStackSessionManager; 181 | -------------------------------------------------------------------------------- /src/lib/chimp-spec.js: -------------------------------------------------------------------------------- 1 | const Chimp = require('./chimp'); 2 | 3 | describe('Chimp', function () { 4 | before(function () { 5 | const emptyFn = function () { 6 | }; 7 | td.replace('async', {}); 8 | td.replace('path', {resolve: emptyFn, join: emptyFn}); 9 | td.replace('chokidar', {}); 10 | td.replace('underscore', {}); 11 | td.replace('./log', {}); 12 | td.replace('freeport', {}); 13 | td.replace('xolvio-ddp', {}); 14 | td.replace('hapi', {}); 15 | td.replace('./ddp-watcher', {}); 16 | td.replace('colors', {}); 17 | td.replace('./boolean-helper', {}); 18 | td.replace('./mocha/mocha.js', {}); 19 | td.replace('./jasmine/jasmine.js', {}); 20 | td.replace('./cucumberjs/cucumber.js', {}); 21 | td.replace('./phantom.js', {}); 22 | td.replace('./chromedriver.js', {}); 23 | td.replace('./consoler.js', {}); 24 | td.replace('./selenium.js', {}); 25 | td.replace('./simian-reporter.js', {}); 26 | }); 27 | after(function () { 28 | td.reset(); 29 | }); 30 | describe('constructor', function () { 31 | beforeEach(function () { 32 | this.chimp = new Chimp(); 33 | }); 34 | it('should create a new instance', function () { 35 | expect(this.chimp).not.to.equal(null); 36 | }); 37 | it('should initialize a processes array', function () { 38 | expect(this.chimp.processes).to.be.instanceof(Array); 39 | }); 40 | it('should initialize an options object if no options are provided', function () { 41 | expect(this.chimp.options).to.be.instanceOf(Object); 42 | }); 43 | it('should store the options object if provided', function () { 44 | const myOptions = {}; 45 | 46 | const chimp = new Chimp(myOptions); 47 | 48 | expect(chimp.options).to.equal(myOptions); 49 | }); 50 | it('should store all the provided options on the environment hash prefixed with [chimp.]', function () { 51 | const myOptions = {a: 1, b: 'aString'}; 52 | const chimp = new Chimp(myOptions); 53 | 54 | expect(process.env['chimp.a']).to.equal(myOptions.a.toString()); 55 | expect(process.env['chimp.b']).to.equal(myOptions.b); 56 | }); 57 | it('puts single ddp option on the environment hash as [chimp.ddp0] if only one provided', function () { 58 | const myOptions = { 59 | ddp: 'http://host:port' 60 | }; 61 | 62 | const chimp = new Chimp(myOptions); 63 | 64 | expect(process.env['chimp.ddp0']).to.equal(myOptions.ddp.toString()); 65 | expect(process.env['chimp.ddp1']).to.be.undefined; 66 | }); 67 | it('puts multiple ddp options on the environment hash as [chimp.ddpX] if multiple provided', function () { 68 | const myOptions = { 69 | ddp: ['http://host:port1', 'http://host:port2'] 70 | }; 71 | 72 | const chimp = new Chimp(myOptions); 73 | 74 | expect(process.env['chimp.ddp0']).to.equal(myOptions.ddp[0].toString()); 75 | expect(process.env['chimp.ddp1']).to.equal(myOptions.ddp[1].toString()); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/lib/chromedriver.js: -------------------------------------------------------------------------------- 1 | var chromedriver = require('chromedriver'), 2 | processHelper = require('./process-helper.js'), 3 | fs = require('fs'), 4 | log = require('./log'); 5 | 6 | /** 7 | * Chromedriver Constructor 8 | * 9 | * @param {Object} options 10 | * @api public 11 | */ 12 | function Chromedriver(options) { 13 | if (!options) { 14 | throw new Error('options is required'); 15 | } 16 | 17 | if (!options.port) { 18 | throw new Error('options.port is required'); 19 | } 20 | 21 | this.options = options; 22 | 23 | this.child = null; 24 | } 25 | 26 | 27 | /** 28 | * Start Chromedriver 29 | * 30 | * @param {Function} callback 31 | * @api public 32 | */ 33 | Chromedriver.prototype.start = function (callback) { 34 | var self = this; 35 | var port = self.options.port; 36 | 37 | if (this.child) { 38 | callback(); 39 | return; 40 | } 41 | 42 | const chromedriverPath = chromedriver.path; 43 | 44 | if (fs.existsSync(chromedriverPath)) { 45 | this.child = processHelper.start({ 46 | bin: chromedriverPath, 47 | prefix: 'chromedriver', 48 | args: ['--port=' + port, '--url-base=wd/hub'], 49 | waitForMessage: /Starting ChromeDriver/, 50 | errorMessage: /Error/ 51 | }, callback); 52 | } 53 | else { 54 | callback('[chimp][chromedriver] Chromedriver executable not found.'); 55 | } 56 | 57 | }; 58 | 59 | /** 60 | * Stop Chromedriver 61 | * 62 | * @param {Function} callback 63 | * @api public 64 | */ 65 | Chromedriver.prototype.stop = function (callback) { 66 | var self = this; 67 | 68 | if (self.child) { 69 | log.debug('[chimp][chromedriver] stopping process'); 70 | 71 | var options = { 72 | child: self.child, 73 | prefix: 'chromedriver' 74 | }; 75 | 76 | processHelper.kill(options, function (err, res) { 77 | self.child = null; 78 | callback(err, res); 79 | }); 80 | 81 | } else { 82 | log.debug('[chimp][chromedriver] no process to kill'); 83 | callback(null); 84 | } 85 | 86 | }; 87 | 88 | /** 89 | * Interrupt Chromedriver 90 | * 91 | * @param {Function} callback 92 | * @api public 93 | */ 94 | Chromedriver.prototype.interrupt = function (callback) { 95 | log.debug('[chimp][chromedriver] interrupt called'); 96 | if (!this.options['watch'] || !!this.options['clean-chromedriver-server']) { 97 | this.stop(callback); 98 | } else { 99 | log.debug('[chimp][chromedriver] interrupt is not killing chromedriver because ' + 100 | 'clean-chromedriver-server not set'); 101 | callback(null); 102 | } 103 | }; 104 | 105 | 106 | 107 | module.exports = Chromedriver; 108 | -------------------------------------------------------------------------------- /src/lib/ci.js: -------------------------------------------------------------------------------- 1 | import {parseBoolean} from './environment-variable-parsers'; 2 | 3 | export function isCI() { 4 | return parseBoolean(process.env.CI); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/consoler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Consoler Constructor 3 | * 4 | * @param {Object} options 5 | * @api public 6 | */ 7 | function Consoler(message, color) { 8 | this.color = color || 'black'; 9 | this.message = message; 10 | } 11 | 12 | /** 13 | * Start Consoler 14 | * 15 | * @param {Function} callback 16 | * @api public 17 | */ 18 | Consoler.prototype.start = function (callback) { 19 | console.log(this.message); 20 | callback(); 21 | }; 22 | 23 | /** 24 | * Stop Consoler 25 | * 26 | * @param {Function} callback 27 | * @api public 28 | */ 29 | Consoler.prototype.stop = function (callback) { 30 | callback(); 31 | }; 32 | 33 | /** 34 | * Interrupt Consoler 35 | * 36 | * @param {Function} callback 37 | * @api public 38 | */ 39 | Consoler.prototype.interrupt = function (callback) { 40 | callback(); 41 | }; 42 | 43 | module.exports = Consoler; 44 | -------------------------------------------------------------------------------- /src/lib/cucumberjs/cucumber-wrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file does some minor modifications on how Cucumber is started. 3 | * 4 | * Modifications to the original code from cucumber: 5 | * - Adds the IPC listener 6 | * - Modify the argv that are passed to the Cucumber CLI 7 | * - Use exit instead of process.exit 8 | */ 9 | 10 | var Cucumber = require('cucumber'); 11 | var exit = require('exit'); 12 | 13 | // This is a modified version of cucumber/lib/cli 14 | function Cli(argv) { 15 | var Cucumber = require('cucumber'); 16 | var Command = require('commander').Command; 17 | var path = require('path'); 18 | 19 | function collect(val, memo) { 20 | memo.push(val); 21 | return memo; 22 | } 23 | 24 | function getProgram() { 25 | var program = new Command(path.basename(argv[1])); 26 | 27 | program 28 | .usage('[options] [...]') 29 | .version(Cucumber.VERSION, '-v, --version') 30 | .option('-b, --backtrace', 'show full backtrace for errors') 31 | .option('--compiler ', 'require files with the given EXTENSION after requiring MODULE (repeatable)', collect, []) 32 | .option('-d, --dry-run', 'invoke formatters without executing steps') 33 | .option('--fail-fast', 'abort the run on first failure') 34 | .option('-f, --format ', 'specify the output format, optionally supply PATH to redirect formatter output (repeatable)', collect, ['pretty']) 35 | .option('--name ', 'only execute the scenarios with name matching the expression (repeatable)', collect, []) 36 | .option('--no-colors', 'disable colors in formatter output') 37 | .option('--no-snippets', 'hide step definition snippets for pending steps') 38 | .option('--no-source', 'hide source uris') 39 | .option('-p, --profile ', 'specify the profile to use (repeatable)', collect, []) 40 | .option('-r, --require ', 'require files before executing features (repeatable)', collect, []) 41 | .option('--snippet-syntax ', 'specify a custom snippet syntax') 42 | .option('-S, --strict', 'fail if there are any undefined or pending steps') 43 | .option('-t, --tags ', 'only execute the features or scenarios with tags matching the expression (repeatable)', collect, []); 44 | 45 | program.on('--help', function () { 46 | console.log(' For more details please visit https://github.com/cucumber/cucumber-js#cli\n'); 47 | }); 48 | 49 | return program; 50 | } 51 | 52 | function getConfiguration() { 53 | var program = getProgram(); 54 | program.parse(argv); 55 | var profileArgs = Cucumber.Cli.ProfilesLoader.getArgs(program.profile); 56 | if (profileArgs.length > 0) { 57 | var fullArgs = argv.slice(0, 2).concat(profileArgs).concat(argv.slice(2)); 58 | program = getProgram(); 59 | program.parse(fullArgs); 60 | } 61 | var configuration = Cucumber.Cli.Configuration(program.opts(), program.args); 62 | return configuration; 63 | } 64 | 65 | var self = { 66 | run: function run(callback) { 67 | var configuration = getConfiguration(); 68 | var runtime = Cucumber.Runtime(configuration); 69 | var formatters = [createIpcFormatter()].concat(configuration.getFormatters()); 70 | formatters.forEach(function (formatter) { 71 | runtime.attachListener(formatter); 72 | }); 73 | try { 74 | runtime.start(callback); 75 | } catch (e) { 76 | console.log(e.stack || e) 77 | callback(false) 78 | } 79 | } 80 | }; 81 | return self; 82 | } 83 | 84 | function createIpcFormatter() { 85 | // add a json formatter that sends results over IPC 86 | const ipcFormatter = new Cucumber.Listener.JsonFormatter(); 87 | const finish = ipcFormatter.finish; 88 | ipcFormatter.finish = function sendResultToChimp(callback) { 89 | finish.call(this, (error, result) => { 90 | const results = this.getLogs(); 91 | process.send(results, () => { 92 | callback(error, result); 93 | }); 94 | }); 95 | }; 96 | return ipcFormatter; 97 | } 98 | 99 | 100 | // This is a modified version of cucumber/bin/cucumber 101 | var argv = process.argv.slice(1); 102 | argv = argv.slice(argv.indexOf('node')); 103 | var cli = Cli(argv); 104 | cli.run(function (succeeded) { 105 | var code = succeeded ? 0 : 2; 106 | 107 | function exitNow() { 108 | exit(code); 109 | } 110 | 111 | if (process.stdout.write('')) { 112 | exitNow(); 113 | } else { 114 | // write() returned false, kernel buffer is not empty yet... 115 | process.stdout.on('drain', exitNow); 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /src/lib/cucumberjs/hooks.js: -------------------------------------------------------------------------------- 1 | const log = require('../log'); 2 | const exit = require('exit'); 3 | const _ = require('underscore'); 4 | const booleanHelper = require('../boolean-helper'); 5 | const screenshotHelper = require('../screenshot-helper'); 6 | 7 | module.exports = function hooks() { 8 | const screenshots = {}; 9 | 10 | this.setDefaultTimeout(60 * 1000); 11 | 12 | this.registerHandler('BeforeFeatures', () => { 13 | log.debug('[chimp][hooks] Starting BeforeFeatures'); 14 | global.chimpHelper.setupBrowserAndDDP(); 15 | global.chimpHelper.createGlobalAliases(); 16 | log.debug('[chimp][hooks] Finished BeforeFeatures'); 17 | // noinspection JSUnresolvedVariable 18 | if (global.UserDefinedBeforeFeatures) { 19 | log.debug('[chimp][hooks] User-defined BeforeFeatures found, calling'); 20 | // noinspection JSUnresolvedFunction 21 | global.UserDefinedBeforeFeatures(); // eslint-disable-line new-cap 22 | } else { 23 | log.debug('[chimp][hooks] User-defined BeforeFeatures not found, finishing up'); 24 | } 25 | }); 26 | 27 | /** 28 | * Capture screenshots either for erroneous / all steps 29 | */ 30 | let lastStep; 31 | this.StepResult((stepResult) => { // eslint-disable-line new-cap 32 | lastStep = stepResult.getStep(); 33 | if (screenshotHelper.shouldTakeScreenshot(stepResult.getStatus())) { 34 | log.debug('[chimp][hooks] capturing screenshot'); 35 | if (booleanHelper.isTruthy(process.env['chimp.saveScreenshotsToReport'])) { 36 | const screenshotId = lastStep.getUri() + ':' + lastStep.getLine(); 37 | // noinspection JSUnresolvedFunction 38 | screenshots[screenshotId] = { 39 | keyword: lastStep.getKeyword(), 40 | name: lastStep.getName(), 41 | uri: lastStep.getUri(), 42 | line: lastStep.getLine(), 43 | png: global.browser.screenshot().value, 44 | }; 45 | } 46 | 47 | if (booleanHelper.isTruthy(process.env['chimp.saveScreenshotsToDisk'])) { 48 | const affix = stepResult.getStatus() !== 'passed' ? ' (failed)' : ''; 49 | // noinspection JSUnresolvedFunction 50 | const fileName = lastStep.getKeyword() + ' ' + lastStep.getName() + affix; 51 | screenshotHelper.saveScreenshotsToDisk(fileName); 52 | } 53 | } 54 | }); 55 | 56 | /** 57 | * Stores captures screenshots in the report 58 | */ 59 | this.After((scenario) => { // eslint-disable-line new-cap 60 | _.each(screenshots, (element) => { 61 | const decodedImage = new Buffer(element.png, 'base64'); 62 | scenario.attach(decodedImage, 'image/png'); 63 | }); 64 | }); 65 | 66 | /** 67 | * After features have run we close the browser and optionally notify 68 | * SauceLabs 69 | */ 70 | this.registerHandler('AfterFeatures', () => { 71 | log.debug('[chimp][hooks] Starting AfterFeatures'); 72 | 73 | if (process.env['chimp.browser'] !== 'phantomjs') { 74 | log.debug('[chimp][hooks] Ending browser session'); 75 | 76 | global.wrapAsync(global.sessionManager.killCurrentSession, global.sessionManager)(); 77 | log.debug('[chimp][hooks] Ended browser sessions'); 78 | } 79 | 80 | log.debug('[chimp][hooks] Finished AfterFeatures'); 81 | }); 82 | 83 | process.on('unhandledRejection', (reason, promise) => { 84 | log.error('[chimp] Detected an unhandledRejection:'.red); 85 | 86 | try { 87 | if (reason.type === 'CommandError' && reason.message === 'Promise never resolved with an truthy value') { 88 | reason.type += 'WebdriverIO CommandError (Promise never resolved with an truthy value)'; 89 | reason.message = 'This usually happens when WebdriverIO assertions fail or timeout.'; 90 | let hint = 'HINT: Check the step AFTER [' + lastStep.getKeyword() + lastStep.getName() + ']'; 91 | let uri = lastStep.getUri(); 92 | uri = uri.substring(process.cwd().length, uri.length); 93 | hint += ' (' + uri + ': >' + lastStep.getLine() + ')'; 94 | log.error('[chimp][hooks] Reason:'.red); 95 | log.error('[chimp][hooks]'.red, reason.type.red); 96 | log.error('[chimp][hooks]'.red, reason.message.red); 97 | log.error(hint.yellow); 98 | reason.message += '\n' + hint; 99 | } else { 100 | log.error('[chimp][hooks]'.red, reason.stack); 101 | } 102 | } catch (e) { 103 | log.debug('[chimp][hooks] Could not provide error hint'); 104 | } 105 | 106 | log.debug('[chimp][hooks] Promise:', JSON.parse(JSON.stringify(promise))); 107 | log.debug('[chimp][hooks] Forcibly exiting Cucumber'); 108 | 109 | process.send(JSON.stringify(reason)); 110 | // Don't exit until the waitUntil uncaught promise bug is fixed in WebdriverIO 111 | // exit(2); 112 | }); 113 | 114 | process.on('SIGINT', () => { 115 | log.debug('[chimp][hooks] Received SIGINT process event, ending browser session'); 116 | global.browser.endAsync().then(() => { 117 | log.debug('[chimp][hooks] ended browser session'); 118 | exit(); 119 | }); 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /src/lib/cucumberjs/world.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | module.exports = function () { 4 | chimpHelper.init(); 5 | chimpHelper.loadAssertionLibrary(); 6 | 7 | var UserWorld = this.World; 8 | this.World = function World() { 9 | this.browser = global.browser; 10 | this.driver = global.browser; 11 | this.client = global.browser; 12 | this.request = global.request; 13 | this.ddp = global.ddp; 14 | this.mirror = global.ddp; 15 | this.server = global.ddp; 16 | this.chimpWidgets = global.chimpWidgets; 17 | 18 | if (UserWorld) { 19 | // The user can modify the world 20 | var userWorldInstance = UserWorld.apply(this, _.toArray(arguments)); 21 | if (userWorldInstance) { 22 | _.extend(this, userWorldInstance); 23 | } 24 | } 25 | 26 | return this; 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/ddp-watcher.js: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/timbotnik/meteor-autoupdate-watcher 2 | 3 | var DDPClient = require('xolvio-ddp'), 4 | _ = require('underscore'), 5 | URL = require('url'), 6 | log = require('./log'); 7 | 8 | var AutoupdateWatcher = function (options) { 9 | this.options = options; 10 | this._url = this._getUrl(options.ddp); 11 | this._currentVersions = {}; 12 | this._lastRerun = 0; 13 | this._autoupdateCollection = {}; 14 | this._ddpClient = null; 15 | }; 16 | 17 | AutoupdateWatcher.prototype._getUrl = function (ddpHost) { 18 | if (ddpHost instanceof Array) { 19 | ddpHost = ddpHost[0]; 20 | } 21 | if (ddpHost.indexOf('http://') === -1 && ddpHost.indexOf('https://') === -1) { 22 | throw new Error('[chimp][ddp] DDP url must contain the protocol'); 23 | } 24 | return URL.parse(ddpHost); 25 | }; 26 | 27 | AutoupdateWatcher.prototype._triggerRerun = function () { 28 | var self = this; 29 | var now = (new Date()).getTime(); 30 | if (now - self._lastRerun > 1000) { 31 | // debounce this since we always see multiple version records change around the same time 32 | // actually rerun here... 33 | self._lastRerun = now; 34 | self._trigger(); 35 | } 36 | }; 37 | 38 | AutoupdateWatcher.prototype._didUpdateVersion = function (doc) { 39 | var self = this; 40 | var versionType; 41 | var versionKey; 42 | if (doc._id.match(/version/) === null) { 43 | versionType = 'version-server'; 44 | versionKey = '_id'; 45 | } else { 46 | versionType = doc._id; 47 | versionKey = 'version'; 48 | } 49 | var prevVersion = self._currentVersions[versionType]; 50 | var newVersion = doc[versionKey]; 51 | var isUpdated = prevVersion && prevVersion !== newVersion; 52 | if (isUpdated) { 53 | log.debug('[chimp][ddp-watcher] New ' + versionType + ': ' + newVersion); 54 | } 55 | self._currentVersions[versionType] = newVersion; 56 | return isUpdated; 57 | }; 58 | 59 | AutoupdateWatcher.prototype._checkForUpdate = function () { 60 | var self = this; 61 | var observedAutoupdate = false; 62 | _.each(self._autoupdateCollection, function (doc) { 63 | if (!observedAutoupdate && self._didUpdateVersion(doc)) { 64 | observedAutoupdate = true; 65 | } 66 | }); 67 | 68 | if (observedAutoupdate) { 69 | self._triggerRerun(); 70 | } 71 | }; 72 | 73 | AutoupdateWatcher.prototype.watch = function (trigger) { 74 | var self = this; 75 | self._trigger = trigger; 76 | self._ddpClient = new DDPClient({ 77 | // All properties optional, defaults shown 78 | host: this._url.hostname, 79 | port: this._url.port, 80 | ssl: this._url.protocol === 'https:', 81 | path: this._url.pathname !== '/' ? this._url.pathname + '/websocket' : undefined, 82 | autoReconnect: true, 83 | autoReconnectTimer: 500, 84 | maintainCollections: true, 85 | ddpVersion: '1', 86 | useSockJs: this._url.pathname === '/' 87 | }); 88 | 89 | /* 90 | * Observe the autoupdate collection. 91 | */ 92 | var observer = self._ddpClient.observe('meteor_autoupdate_clientVersions'); 93 | observer.added = function (id) { 94 | log.debug('[chimp][ddp-watcher] ADDED to ' + observer.name + ': ' + id); 95 | }; 96 | observer.changed = function (id, oldFields, clearedFields, newFields) { 97 | log.debug('[chimp][ddp-watcher] CHANGED in ' + observer.name + ': ' + id); 98 | log.debug('[chimp][ddp-watcher] CHANGED old field values: ', oldFields); 99 | log.debug('[chimp][ddp-watcher] CHANGED cleared fields: ', clearedFields); 100 | log.debug('[chimp][ddp-watcher] CHANGED new fields: ', newFields); 101 | if (self._didUpdateVersion(self._autoupdateCollection[id])) { 102 | self._triggerRerun(); 103 | } 104 | }; 105 | observer.removed = function (id, oldValue) { 106 | log.debug('[chimp][ddp-watcher] REMOVED in ' + observer.name + ': ' + id); 107 | log.debug('[chimp][ddp-watcher] REMOVED previous value: ', oldValue); 108 | }; 109 | self._observer = observer; 110 | 111 | /* 112 | * Connect to the Meteor Server 113 | */ 114 | 115 | self._ddpClient.connect(function (error, wasReconnect) { 116 | 117 | // If autoReconnect is true, this callback will be invoked each time 118 | // a server connection is re-established 119 | if (error) { 120 | log.error('[chimp][ddp-watcher] DDP connection error!', error); 121 | self._isConnected = false; 122 | return; 123 | } 124 | self._isConnected = true; 125 | 126 | if (wasReconnect) { 127 | log.debug('[chimp][ddp-watcher] Reconnected'); 128 | } else { 129 | log.debug('[chimp][ddp-watcher] Connected'); 130 | } 131 | 132 | // force a reset of 'maintained' collections 133 | self._ddpClient.collections = {}; 134 | 135 | /* 136 | * Subscribe to the Meteor Autoupdate Collection 137 | */ 138 | self._subscriptionHandle = self._ddpClient.subscribe('meteor_autoupdate_clientVersions', [], 139 | function () { // callback when the subscription is ready 140 | self._autoupdateCollection = self._ddpClient.collections.meteor_autoupdate_clientVersions; 141 | log.debug('[chimp][ddp-watcher] meteor_autoupdate_clientVersions ready:'); 142 | log.debug('[chimp][ddp-watcher] ' + self._autoupdateCollection); 143 | self._checkForUpdate(); 144 | } 145 | ); 146 | }); 147 | }; 148 | 149 | 150 | module.exports = AutoupdateWatcher; 151 | -------------------------------------------------------------------------------- /src/lib/ddp.js: -------------------------------------------------------------------------------- 1 | var log = require('./log'), 2 | URL = require('url'), 3 | DDPClient = require('xolvio-ddp'), 4 | booleanHelper = require('./boolean-helper'), 5 | wrapAsyncObject = require('xolvio-sync-webdriverio').wrapAsyncObject; 6 | 7 | /** 8 | * DDP Constructor 9 | * 10 | * @api public 11 | */ 12 | function DDP(url) { 13 | log.debug('[chimp][ddp] creating DDP wrapper'); 14 | process.env.ROOT_URL = process.env.ROOT_URL || process.env['chimp.ddp0']; 15 | 16 | if (!url) { 17 | this.url = this._getUrl(process.env['chimp.ddp0']); 18 | } else { 19 | this.url = this._getUrl(url); 20 | } 21 | } 22 | 23 | DDP.prototype.connect = function () { 24 | var options = this._getOptions(); 25 | log.debug('[chimp][ddp] Connecting to DDP server', options); 26 | return wrapAsyncObject( 27 | new DDPClient(options), 28 | ['connect', 'call', 'apply', 'callWithRandomSeed', 'subscribe'], 29 | {syncByDefault: booleanHelper.isTruthy(process.env['chimp.sync'])} 30 | ); 31 | }; 32 | 33 | DDP.prototype._getUrl = function (ddpHost) { 34 | if (ddpHost.indexOf('http://') === -1 && ddpHost.indexOf('https://') === -1) { 35 | throw new Error('[chimp][ddp] DDP url must contain the protocol'); 36 | } 37 | return URL.parse(ddpHost); 38 | }; 39 | 40 | 41 | DDP.prototype._getOptions = function () { 42 | return { 43 | host: this.url.hostname, 44 | port: this.url.port, 45 | ssl: this.url.protocol === 'https:', 46 | path: this.url.pathname !== '/' ? this.url.pathname + '/websocket' : undefined, 47 | // TODO extract all options 48 | autoReconnect: true, 49 | autoReconnectTimer: 500, 50 | maintainCollections: true, 51 | ddpVersion: '1', 52 | useSockJs: this.url.pathname === '/' 53 | }; 54 | }; 55 | 56 | module.exports = DDP; 57 | -------------------------------------------------------------------------------- /src/lib/environment-variable-parsers.js: -------------------------------------------------------------------------------- 1 | import {isTruthy} from './boolean-helper'; 2 | 3 | export function parseString(value) { 4 | return value; 5 | } 6 | 7 | export function parseInteger(value) { 8 | return parseInt(value, 10); 9 | } 10 | 11 | export function parseNumber(value) { 12 | return Number(value); 13 | } 14 | 15 | export function parseBoolean(value) { 16 | return isTruthy(value); 17 | } 18 | 19 | export function parseNullable(value, convert) { 20 | return value === 'null' ? null : convert(value); 21 | } 22 | 23 | export function parseNullableString(value) { 24 | return parseNullable(value, parseString); 25 | } 26 | 27 | export function parseNullableInteger(value) { 28 | return parseNullable(value, parseInteger); 29 | } 30 | 31 | export function parseNullableNumber(value) { 32 | return parseNullable(value, parseNumber); 33 | } 34 | 35 | export function parseNullableBoolean(value) { 36 | return parseNullable(value, parseBoolean); 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/jasmine/jasmine-fiberized-api.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import {fiberize, fiberizeSync} from '../utils/fiberize'; 3 | 4 | export default function fiberizeJasmineApi(context) { 5 | ['describe', 'xdescribe', 'fdescribe'].forEach(function (method) { 6 | const original = context[method]; 7 | context[method] = _.wrap(original, function (fn) { 8 | const args = Array.prototype.slice.call(arguments, 1); 9 | if (_.isFunction(_.last(args))) { 10 | args.push(fiberizeSync(args.pop())); 11 | } 12 | return fn.apply(this, args); 13 | }); 14 | }); 15 | 16 | [ 17 | 'it', 'xit', 'fit', 18 | 'beforeEach', 'afterEach', 19 | 'beforeAll', 'afterAll', 20 | ].forEach(function (method) { 21 | const original = context[method]; 22 | context[method] = _.wrap(original, function (fn) { 23 | const args = Array.prototype.slice.call(arguments, 1); 24 | if (_.isFunction(_.last(args))) { 25 | args.push(fiberize(args.pop())); 26 | } 27 | return fn.apply(this, args); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/jasmine/jasmine-helpers.js: -------------------------------------------------------------------------------- 1 | import chimpHelper from '../chimp-helper'; 2 | import log from '../log'; 3 | 4 | beforeAll(function chimpSetup() { 5 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; 6 | chimpHelper.init(); 7 | chimpHelper.setupBrowserAndDDP(); 8 | }); 9 | 10 | afterAll(function chimpTeardown() { 11 | if (process.env['chimp.browser'] !== 'phantomjs') { 12 | log.debug('[chimp][jasmine-helpers] Ending browser session'); 13 | global.wrapAsync(global.sessionManager.killCurrentSession, global.sessionManager)(); 14 | log.debug('[chimp][jasmine-helpers] Ended browser sessions'); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/jasmine/jasmine-wrapper.js: -------------------------------------------------------------------------------- 1 | require('../babel-register'); 2 | 3 | import path from 'path'; 4 | import Fiber from 'fibers'; 5 | import _ from 'underscore'; 6 | import {parseBoolean, parseString } from '../environment-variable-parsers'; 7 | import escapeRegExp from '../utils/escape-reg-exp'; 8 | import fiberizeJasmineApi from './jasmine-fiberized-api'; 9 | import screenshotHelper from '../screenshot-helper'; 10 | import booleanHelper from '../boolean-helper'; 11 | 12 | new Fiber(function runJasmineInFiber() { 13 | const projectDir = process.env.PWD; 14 | const testsDir = process.env['chimp.path']; 15 | process.chdir(testsDir); 16 | 17 | const Jasmine = require('jasmine'); 18 | const jasmine = new Jasmine(); 19 | 20 | // Capability to add multiple spec filters 21 | const specFilters = []; 22 | jasmine.env.specFilter = function shouldRunSpec(spec) { 23 | return _.every(specFilters, specFilter => specFilter(spec)); 24 | }; 25 | 26 | jasmine.jasmine.addSpecFilter = function addSpecFilter(filterFn) { 27 | specFilters.push(filterFn); 28 | }; 29 | 30 | if (parseBoolean(process.env['chimp.watch'])) { 31 | // Only run specs with a watch tag in watch mode 32 | const watchedSpecRegExp = new RegExp( 33 | parseString(process.env['chimp.watchTags']).split(',').map(escapeRegExp).join('|') 34 | ); 35 | jasmine.jasmine.addSpecFilter((spec) => watchedSpecRegExp.test(spec.getFullName())); 36 | } 37 | 38 | // Capability to capture screenshots 39 | jasmine.jasmine.getEnv().addReporter({ 40 | specDone: function(result) { 41 | if (screenshotHelper.shouldTakeScreenshot(result.status)) { 42 | if (booleanHelper.isTruthy(process.env['chimp.saveScreenshotsToDisk'])) { 43 | const affix = result.status !== 'passed' ? ' (failed)' : ''; 44 | const fileName = result.fullName + affix; 45 | screenshotHelper.saveScreenshotsToDisk(fileName, projectDir); 46 | } 47 | } 48 | } 49 | }); 50 | 51 | fiberizeJasmineApi(global); 52 | 53 | jasmine.loadConfig(getJasmineConfig()); 54 | jasmine.configureDefaultReporter( 55 | JSON.parse(process.env['chimp.jasmineReporterConfig']) 56 | ); 57 | jasmine.execute(); 58 | }).run(); 59 | 60 | 61 | function getJasmineConfig() { 62 | const jasmineConfig = JSON.parse(process.env['chimp.jasmineConfig']); 63 | 64 | if (jasmineConfig.specDir) { 65 | if (!jasmineConfig.spec_dir) { 66 | jasmineConfig.spec_dir = jasmineConfig.specDir; 67 | } 68 | delete jasmineConfig.specDir; 69 | } 70 | 71 | if (jasmineConfig.specFiles) { 72 | if (!jasmineConfig.spec_files) { 73 | jasmineConfig.spec_files = jasmineConfig.specFiles; 74 | } 75 | delete jasmineConfig.specFiles; 76 | } 77 | 78 | if (!jasmineConfig.helpers) { 79 | jasmineConfig.helpers = []; 80 | } 81 | jasmineConfig.helpers.unshift( 82 | path.relative(jasmineConfig.spec_dir, path.resolve(__dirname, 'jasmine-helpers.js')) 83 | ); 84 | 85 | return jasmineConfig; 86 | } 87 | -------------------------------------------------------------------------------- /src/lib/jasmine/jasmine.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | cp = require('child-process-debug'), 3 | processHelper = require('./../process-helper.js'), 4 | booleanHelper = require('../boolean-helper'), 5 | log = require('./../log'), 6 | _ = require('underscore'), 7 | colors = require('colors'), 8 | glob = require('glob'), 9 | fs = require('fs-extra'); 10 | 11 | /** 12 | * Mocha Constructor 13 | * 14 | * @param {Object} options 15 | * @api public 16 | */ 17 | 18 | function Jasmine(options) { 19 | this.options = options; 20 | this.child = null; 21 | } 22 | 23 | /** 24 | * Run Mocha specs 25 | * 26 | * @param {Function} callback 27 | * @api public 28 | */ 29 | 30 | Jasmine.prototype.start = function (callback) { 31 | 32 | var self = this; 33 | if (glob.sync(self.options.path).length === 0) { 34 | const infoMessage = `[chimp][jasmine] Directory ${self.options.path} does not exist. Not running`; 35 | if (booleanHelper.isTruthy(this.options['fail-when-no-tests-run'])) { 36 | callback(infoMessage); 37 | } 38 | else { 39 | log.info(infoMessage); 40 | callback(); 41 | } 42 | return; 43 | } 44 | 45 | log.debug('[chimp][jasmine] Running...'); 46 | 47 | var opts = { 48 | env: process.env, 49 | silent: true 50 | }; 51 | 52 | var port; 53 | if (this.options.debugJasmine) { 54 | port = parseInt(this.options.debugJasmine); 55 | if (port > 1) { 56 | opts.execArgv = ['--debug=' + port]; 57 | } else { 58 | opts.execArgv = ['--debug']; 59 | } 60 | } 61 | 62 | if (this.options.debugBrkMocha) { 63 | port = parseInt(this.options.debugBrkMocha); 64 | if (port > 1) { 65 | opts.execArgv = ['--debug-brk=' + port]; 66 | } else { 67 | opts.execArgv = ['--debug-brk']; 68 | } 69 | } 70 | 71 | self.child = cp.fork(path.join(__dirname, 'jasmine-wrapper.js'), [], opts); 72 | 73 | self.child.stdout.pipe(process.stdout); 74 | self.child.stderr.pipe(process.stderr); 75 | process.stdin.pipe(self.child.stdin); 76 | 77 | let noTestsFound = false; 78 | self.child.stdout.on('data', function(data) { 79 | const colorCodesRegExp = new RegExp(`\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`, 'g'); 80 | const dataFromStdout = data.toString().replace(colorCodesRegExp, '').trim(); 81 | if (/^No specs found/.test(dataFromStdout)) { 82 | noTestsFound = true; 83 | } 84 | }); 85 | 86 | var result = null; 87 | self.child.on('message', function (res) { 88 | log.debug('[chimp][jasmine] Received message from Jasmine child. Result:', res); 89 | result = res; 90 | }); 91 | 92 | self.child.on('close', (code) => { 93 | log.debug('[chimp][jasmine] Closed with code', code); 94 | const failWhenNoTestsRun = booleanHelper.isTruthy(self.options['fail-when-no-tests-run']); 95 | if (!self.child.stopping) { 96 | log.debug('[chimp][jasmine] Jasmine not in a stopping state'); 97 | callback(code !== 0 || (code === 0 && noTestsFound && failWhenNoTestsRun) ? 'Jasmine failed' : null, result); 98 | } 99 | }); 100 | 101 | }; 102 | 103 | Jasmine.prototype.interrupt = function (callback) { 104 | 105 | log.debug('[chimp][jasmine] interrupting jasmine'); 106 | 107 | var self = this; 108 | 109 | if (!self.child) { 110 | log.debug('[chimp][jasmine] no child to interrupt'); 111 | return callback(); 112 | } 113 | self.child.stopping = true; 114 | 115 | var options = { 116 | child: self.child, 117 | prefix: 'jasmine' 118 | }; 119 | 120 | processHelper.kill(options, function (err, res) { 121 | self.child = null; 122 | if (callback) { 123 | callback(err, res); 124 | } 125 | }); 126 | 127 | }; 128 | 129 | module.exports = Jasmine; 130 | -------------------------------------------------------------------------------- /src/lib/log.js: -------------------------------------------------------------------------------- 1 | var log = require('loglevel'), 2 | minimist = require('minimist'); 3 | 4 | var argv = minimist(process.argv, { 5 | 'default': { 6 | 'debug': false, 7 | 'log': 'info' 8 | }, 'boolean': true 9 | }) || []; 10 | 11 | var debug = 12 | process.env['chimp.debug'] === 'true' ? true : 13 | process.env['chimp.debug'] === 'false' ? false : 14 | process.env['chimp.debug'] 15 | || process.env.DEBUG === 'true' 16 | || process.env.DEBUG === '1' 17 | || argv.debug; 18 | 19 | if (debug) { 20 | log.setLevel('debug'); 21 | } else { 22 | log.setLevel(argv.log); 23 | } 24 | module.exports = log; 25 | -------------------------------------------------------------------------------- /src/lib/mocha/mocha-fiberized-ui.js: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/tzeskimo/mocha-fibers/blob/master/lib/mocha-fibers.js 2 | 3 | // A copy of bdd interface, but beforeEach, afterEach, before, after, 4 | // and it methods all run within fibers. 5 | var Mocha = require('mocha'), 6 | bdd = Mocha.interfaces.bdd, 7 | _ = require('underscore'), 8 | util = require('util'); 9 | 10 | import {fiberize} from '../utils/fiberize'; 11 | 12 | // A copy of bdd interface, but wrapping everything in fibers 13 | module.exports = Mocha.interfaces['fiberized-bdd-ui'] = function (suite) { 14 | bdd(suite); 15 | suite.on('pre-require', function (context) { 16 | // Wrap test related methods in fiber 17 | ['beforeEach', 'afterEach', 'after', 'before', 'it'].forEach(function (method) { 18 | var original = context[method]; 19 | context[method] = _.wrap(original, function (fn) { 20 | var args = Array.prototype.slice.call(arguments, 1); 21 | if (_.isFunction(_.last(args))) { 22 | args.push(fiberize(args.pop())); 23 | } 24 | return fn.apply(this, args); 25 | }); 26 | _.extend(context[method], _(original).pick('only', 'skip')); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/mocha/mocha-helper.js: -------------------------------------------------------------------------------- 1 | var chimpHelper = require('../chimp-helper'); 2 | var exit = require('exit'); 3 | var log = require('../log'); 4 | const booleanHelper = require('../boolean-helper'); 5 | const screenshotHelper = require('../screenshot-helper'); 6 | 7 | before(function () { 8 | process.env['chimp.chai'] = true; 9 | chimpHelper.loadAssertionLibrary(); 10 | chimpHelper.init(); 11 | chimpHelper.setupBrowserAndDDP(); 12 | }); 13 | 14 | after(function () { 15 | if (process.env['chimp.browser'] !== 'phantomjs') { 16 | log.debug('[chimp][mocha-helper] Ending browser session'); 17 | global.wrapAsync(global.sessionManager.killCurrentSession, global.sessionManager)(); 18 | log.debug('[chimp][mocha-helper] Ended browser sessions'); 19 | } 20 | }); 21 | 22 | afterEach(function () { 23 | if (screenshotHelper.shouldTakeScreenshot(this.currentTest.state)) { 24 | if (booleanHelper.isTruthy(process.env['chimp.saveScreenshotsToDisk'])) { 25 | const affix = this.currentTest.state !== 'passed' ? ' (failed)' : ''; 26 | const fileName = this.currentTest.fullTitle() + affix; 27 | screenshotHelper.saveScreenshotsToDisk(fileName); 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/lib/mocha/mocha-wrapper-instance.js: -------------------------------------------------------------------------------- 1 | import { MochaWrapper } from './mocha-wrapper'; 2 | 3 | const mochaWrapper = new MochaWrapper(); 4 | -------------------------------------------------------------------------------- /src/lib/mocha/mocha-wrapper-spec.js: -------------------------------------------------------------------------------- 1 | // const testDir = './tests/'; 2 | // const testDirFiles = ['fileA.js', 'fileB.js']; 3 | // const filesGlobValue = './lib/**/*.spec.js'; 4 | // const filesGlobFiles = ['./lib/fileA.spec.js', './lib/someDir/fileB.spec.js']; 5 | // const emptyTestsDirPath = ''; 6 | // 7 | // describe('mocha-wrapper', function () { 8 | // beforeEach(function () { 9 | // td.replace('./mocha-fiberized-ui', {}); 10 | // td.replace('exit', td.function('exit')); 11 | // td.replace('../babel-register', td.object({})); 12 | // const emptyFn = () => {}; 13 | // this.mocha = td.replace('mocha'); 14 | // this.path = td.replace('path', td.object({ join: emptyFn, resolve: emptyFn })); 15 | // this.glob = td.replace('glob', td.object({ sync: emptyFn })); 16 | // process.env.mochaConfig = JSON.stringify({ tags: ''}); 17 | // process.argv = []; 18 | // }); 19 | // afterEach(function() { 20 | // td.reset(); 21 | // }); 22 | // describe('add files', function () { 23 | // it('adds files inside testDir', function () { 24 | // process.argv = []; 25 | // process.env['chimp.path'] = testDir; 26 | // // mock --path tests dir files 27 | // td.when(this.path.join('mocha-helper.js')).thenReturn('mocha-helper.js'); 28 | // td.when(this.path.resolve(__dirname, 'mocha-helper.js')).thenReturn('mocha-helper.js'); 29 | // td.when(this.path.join(testDir, '**')).thenReturn(testDir); 30 | // td.when(this.glob.sync(testDir)).thenReturn(testDirFiles); 31 | // td.when(this.mocha.run(td.matchers.anything())).thenReturn(null); 32 | // 33 | // const MochaWrapper = require('./mocha-wrapper.js').MochaWrapper; 34 | // const mochaWrapper = new MochaWrapper(); 35 | // 36 | // td.verify(this.mocha.addFile('fileA.js')); 37 | // td.verify(this.mocha.addFile('fileB.js')); 38 | // td.verify(this.mocha.addFile('mocha-helper.js')); 39 | // td.verify(this.mocha.addFile(), {times: 3, ignoreExtraArgs: true}); // verify only called three times 40 | // }); 41 | // it('adds files specified by --files options when --path is empty dir', function() { 42 | // process.argv = []; 43 | // process.env['chimp.path'] = emptyTestsDirPath; 44 | // process.env['chimp.files'] = filesGlobValue; 45 | // // mock empty --path tests dir files 46 | // td.when(this.path.join('mocha-helper.js')).thenReturn('mocha-helper.js'); 47 | // td.when(this.path.resolve(__dirname, 'mocha-helper.js')).thenReturn('mocha-helper.js'); 48 | // td.when(this.path.join(emptyTestsDirPath, '**')).thenReturn(emptyTestsDirPath); 49 | // td.when(this.glob.sync(emptyTestsDirPath)).thenReturn([]); 50 | // // mock --files flag files 51 | // td.when(this.glob.sync(filesGlobValue)).thenReturn(filesGlobFiles); 52 | // 53 | // const MochaWrapper = require('./mocha-wrapper.js').MochaWrapper; 54 | // const mochaWrapper = new MochaWrapper(); 55 | // 56 | // td.verify(this.mocha.addFile('./lib/fileA.spec.js')); 57 | // td.verify(this.mocha.addFile('./lib/someDir/fileB.spec.js')); 58 | // td.verify(this.mocha.addFile('mocha-helper.js')); 59 | // td.verify(this.mocha.addFile(), {times: 3, ignoreExtraArgs: true}); 60 | // }); 61 | // it('adds files specified by --files option and files in --path when both contain files', function() { 62 | // process.argv = []; 63 | // process.env['chimp.path'] = testDir; 64 | // process.env['chimp.files'] = filesGlobValue; 65 | // // mock mocha-helper 66 | // td.when(this.path.join('mocha-helper.js')).thenReturn('mocha-helper.js'); 67 | // td.when(this.path.resolve(__dirname, 'mocha-helper.js')).thenReturn('mocha-helper.js'); 68 | // // mock --files flag files 69 | // td.when(this.glob.sync(filesGlobValue)).thenReturn(filesGlobFiles); 70 | // // mock --path test dir files 71 | // td.when(this.path.join(testDir, '**')).thenReturn(testDir); 72 | // td.when(this.glob.sync(testDir)).thenReturn(testDirFiles); 73 | // td.when(this.mocha.run(td.matchers.anything())).thenReturn(null); 74 | // 75 | // const MochaWrapper = require('./mocha-wrapper.js').MochaWrapper; 76 | // const mochaWrapper = new MochaWrapper(); 77 | // 78 | // td.verify(this.mocha.addFile('fileA.js')); 79 | // td.verify(this.mocha.addFile('fileB.js')); 80 | // td.verify(this.mocha.addFile('./lib/fileA.spec.js')); 81 | // td.verify(this.mocha.addFile('./lib/someDir/fileB.spec.js')); 82 | // td.verify(this.mocha.addFile('mocha-helper.js')); 83 | // td.verify(this.mocha.addFile(), {times: 5, ignoreExtraArgs: true}); 84 | // }); 85 | // it('adds files specified on cli directly', function() { 86 | // process.argv = ['/usr/local/bin/node', 'chimp', '--mocha', 'test/fileA.js', 'test/fileB.js', 'test/fileC.js', 'fileD.js']; 87 | // // mock mocha-helper 88 | // td.when(this.path.join('mocha-helper.js')).thenReturn('mocha-helper.js'); 89 | // td.when(this.path.resolve(__dirname, 'mocha-helper.js')).thenReturn('mocha-helper.js'); 90 | // 91 | // const MochaWrapper = require('./mocha-wrapper.js').MochaWrapper; 92 | // const mochaWrapper = new MochaWrapper(); 93 | // td.verify(this.mocha.addFile('test/fileA.js')); 94 | // td.verify(this.mocha.addFile('test/fileB.js')); 95 | // td.verify(this.mocha.addFile('test/fileC.js')); 96 | // td.verify(this.mocha.addFile('fileD.js')); 97 | // td.verify(this.mocha.addFile('mocha-helper.js')); 98 | // td.verify(this.mocha.addFile(), {times: 5, ignoreExtraArgs: true}); 99 | // 100 | // }); 101 | // }); 102 | // }); 103 | -------------------------------------------------------------------------------- /src/lib/mocha/mocha-wrapper.js: -------------------------------------------------------------------------------- 1 | require('../babel-register'); 2 | var Mocha = require('mocha'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | exit = require('exit'), 6 | glob = require('glob'), 7 | ui = require('./mocha-fiberized-ui'), 8 | _ = require('underscore'); 9 | 10 | import {parseBoolean, parseNullableString, parseString} from '../environment-variable-parsers'; 11 | import escapeRegExp from '../utils/escape-reg-exp'; 12 | 13 | class MochaWrapper { 14 | constructor() { 15 | let mochaConfig = JSON.parse(process.env.mochaConfig); 16 | const mochaCommandLineOptions = process.env['chimp.mochaCommandLineOptions'] ? JSON.parse(process.env['chimp.mochaCommandLineOptions']) : false; 17 | if (mochaCommandLineOptions && _.isObject(mochaCommandLineOptions)) { 18 | _.extend(mochaConfig, mochaCommandLineOptions); 19 | } 20 | mochaConfig.ui = 'fiberized-bdd-ui'; 21 | if (!mochaConfig.grep && parseBoolean(process.env['chimp.watch'])) { 22 | mochaConfig.grep = new RegExp(parseString(process.env['chimp.watchTags']).split(',').map(escapeRegExp).join('|')); 23 | } else if (!mochaConfig.grep) { 24 | mochaConfig.grep = new RegExp( 25 | parseString(mochaConfig.tags).split(',').map(escapeRegExp).join('|') 26 | ); 27 | } 28 | 29 | var mocha = new Mocha(mochaConfig); 30 | 31 | mocha.addFile(path.join(path.resolve(__dirname, path.join('mocha-helper.js')))); 32 | if (process.argv.length > 3) { 33 | process.argv.splice(3).forEach(function (spec) { 34 | mocha.addFile(spec); 35 | }); 36 | } else { 37 | // Add each .js file in the tests dir to the mocha instance 38 | var testDir = process.env['chimp.path']; 39 | glob.sync(path.join(testDir, '**')).filter(function (file) { 40 | // Only keep the .js files 41 | return file.substr(-3) === '.js'; 42 | }).forEach(function (file) { 43 | mocha.addFile(file); 44 | }); 45 | 46 | if (process.env['chimp.files']) { 47 | // Add each file specified by the "files" option to the mocha instance 48 | glob.sync(process.env['chimp.files']).forEach(function (file) { 49 | mocha.addFile(file); 50 | }); 51 | } 52 | 53 | } 54 | 55 | try { 56 | // Run the tests. 57 | mocha.run(function (failures) { 58 | exit(failures); 59 | }); 60 | } catch (e) { 61 | throw (e); 62 | } 63 | } 64 | } 65 | 66 | export { 67 | MochaWrapper 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/mocha/mocha.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | cp = require('child-process-debug'), 3 | processHelper = require('./../process-helper.js'), 4 | booleanHelper = require('../boolean-helper'), 5 | log = require('./../log'), 6 | _ = require('underscore'), 7 | colors = require('colors'), 8 | glob = require('glob'), 9 | fs = require('fs-extra'); 10 | 11 | /** 12 | * Mocha Constructor 13 | * 14 | * @param {Object} options 15 | * @api public 16 | */ 17 | 18 | function Mocha(options) { 19 | this.options = options; 20 | this.child = null; 21 | } 22 | 23 | /** 24 | * Run Mocha specs 25 | * 26 | * @param {Function} callback 27 | * @api public 28 | */ 29 | 30 | Mocha.prototype.start = function (callback) { 31 | 32 | var self = this; 33 | if (glob.sync(self.options.path).length === 0) { 34 | const infoMessage = `[chimp][mocha] Directory ${self.options.path} does not exist. Not running`; 35 | if (booleanHelper.isTruthy(self.options['fail-when-no-tests-run'])) { 36 | callback(infoMessage); 37 | } 38 | else { 39 | log.info(infoMessage); 40 | callback(); 41 | } 42 | return; 43 | } 44 | 45 | log.debug('[chimp][mocha] Running...'); 46 | 47 | var opts = { 48 | env: process.env, 49 | silent: true 50 | }; 51 | 52 | var port; 53 | if (this.options.debugMocha) { 54 | port = parseInt(this.options.debugMocha); 55 | if (port > 1) { 56 | opts.execArgv = ['--debug=' + port]; 57 | } else { 58 | opts.execArgv = ['--debug']; 59 | } 60 | } 61 | 62 | if (this.options.debugBrkMocha) { 63 | port = parseInt(this.options.debugBrkMocha); 64 | if (port > 1) { 65 | opts.execArgv = ['--debug-brk=' + port]; 66 | } else { 67 | opts.execArgv = ['--debug-brk']; 68 | } 69 | } 70 | 71 | if (this.options.inspectMocha) { 72 | port = parseInt(this.options.inspectMocha); 73 | if (port > 1) { 74 | opts.execArgv = ['--inspect=' + port]; 75 | } else { 76 | opts.execArgv = ['--inspect']; 77 | } 78 | } 79 | 80 | if (this.options.inspectBrkMocha) { 81 | port = parseInt(this.options.inspectBrkMocha); 82 | if (port > 1) { 83 | opts.execArgv = ['--inspect-brk=' + port]; 84 | } else { 85 | opts.execArgv = ['--inspect-brk']; 86 | } 87 | } 88 | 89 | let _specs = []; 90 | if (this.options._ && this.options._.length > 2) { 91 | _specs = this.options._.slice(2); 92 | } 93 | 94 | opts.env = _.extend(process.env, { 95 | mochaConfig: JSON.stringify(this.options.mochaConfig) 96 | }); 97 | self.child = cp.fork(path.join(__dirname, 'mocha-wrapper-instance.js'), _specs, opts); 98 | self.child.stdout.pipe(process.stdout); 99 | self.child.stderr.pipe(process.stderr); 100 | process.stdin.pipe(self.child.stdin); 101 | 102 | 103 | let noTestsFound = false; 104 | self.child.stdout.on('data', function(data) { 105 | const colorCodesRegExp = new RegExp(`\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`, 'g'); 106 | const dataFromStdout = data.toString().replace(colorCodesRegExp, '').trim(); 107 | if (/^0 passing/.test(dataFromStdout)) { 108 | noTestsFound = true; 109 | } 110 | }); 111 | 112 | var result = null; 113 | self.child.on('message', function (res) { 114 | log.debug('[chimp][mocha] Received message from Mocha child. Result:', res); 115 | result = res; 116 | }); 117 | 118 | self.child.on('close', (code) => { 119 | log.debug('[chimp][mocha] Closed with code', code); 120 | const failWhenNoTestsRun = booleanHelper.isTruthy(self.options['fail-when-no-tests-run']); 121 | if (!self.child.stopping) { 122 | log.debug('[chimp][mocha] Mocha not in a stopping state'); 123 | callback(code !== 0 || (code === 0 && noTestsFound && failWhenNoTestsRun) ? 'Mocha failed' : null, result); 124 | } 125 | }); 126 | 127 | }; 128 | 129 | Mocha.prototype.interrupt = function (callback) { 130 | 131 | log.debug('[chimp][mocha] interrupting mocha'); 132 | 133 | var self = this; 134 | 135 | if (!self.child) { 136 | log.debug('[chimp][mocha] no child to interrupt'); 137 | return callback(); 138 | } 139 | self.child.stopping = true; 140 | 141 | var options = { 142 | child: self.child, 143 | prefix: 'mocha' 144 | }; 145 | 146 | processHelper.kill(options, function (err, res) { 147 | self.child = null; 148 | if (callback) { 149 | callback(err, res); 150 | } 151 | }); 152 | 153 | }; 154 | 155 | module.exports = Mocha; 156 | -------------------------------------------------------------------------------- /src/lib/options-loader.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const log = require('./log'); 4 | import merge from 'deep-extend'; 5 | 6 | module.exports = { 7 | getOptions() { 8 | let userOptionsFile; 9 | const processArgv = this._getProcessArgv(); 10 | if (processArgv[2] && processArgv[2].match(/.*chimp.*\.js$/)) { 11 | userOptionsFile = path.resolve(process.cwd(), processArgv[2]); 12 | processArgv.splice(2, 1); 13 | if (!fs.existsSync(userOptionsFile)) { 14 | log.error(('[chimp] Could not find ' + processArgv[2]).red); 15 | this._exit(1); 16 | } 17 | } else { 18 | userOptionsFile = path.resolve(process.cwd(), 'chimp.js'); 19 | } 20 | 21 | let userOptions = {}; 22 | if (fs.existsSync(userOptionsFile)) { 23 | userOptions = this._requireFile(userOptionsFile); 24 | log.debug('[chimp] loaded', userOptionsFile); 25 | } 26 | const defaultOptions = this._requireFile(this._getDefaultConfigFilePath()); 27 | const options = merge(defaultOptions, userOptions); 28 | log.debug('[chimp] Chimp options are', options); 29 | return options; 30 | }, 31 | _requireFile(file) { 32 | return require(file); 33 | }, 34 | _getProcessArgv() { 35 | return process.argv; 36 | }, 37 | _exit(code) { 38 | process.exit(code); 39 | }, 40 | _getDefaultConfigFilePath() { 41 | return path.resolve(__dirname, '..', 'bin', 'default.js'); 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/phantom.js: -------------------------------------------------------------------------------- 1 | var phantomjs = require('phantomjs-prebuilt'), 2 | processHelper = require('./process-helper.js'), 3 | log = require('./log'); 4 | 5 | /** 6 | * Phantom Constructor 7 | * 8 | * @param {Object} options 9 | * @api public 10 | */ 11 | function Phantom(options) { 12 | if (!options) { 13 | throw new Error('options is required'); 14 | } 15 | 16 | if (!options.port) { 17 | throw new Error('options.port is required'); 18 | } 19 | 20 | this.options = options; 21 | 22 | this.child = null; 23 | } 24 | 25 | /** 26 | * Start Phantom 27 | * 28 | * @param {Function} callback 29 | * @api public 30 | */ 31 | Phantom.prototype.start = function (callback) { 32 | var self = this; 33 | var port = self.options.port; 34 | var ignoreSSLErrors = self.options.phantom_ignoreSSLErrors || 'false'; 35 | 36 | if (this.child) { 37 | callback(); 38 | return; 39 | } 40 | 41 | this.child = processHelper.start({ 42 | bin: process.env['chimp.phantom_path'] || phantomjs.path, 43 | prefix: 'phantom', 44 | args: ['--webdriver', port, '--ignore-ssl-errors', ignoreSSLErrors], 45 | waitForMessage: /GhostDriver - Main - running on port/, 46 | errorMessage: /Error/ 47 | }, callback); 48 | 49 | }; 50 | 51 | /** 52 | * Stop Phantom 53 | * 54 | * @param {Function} callback 55 | * @api public 56 | */ 57 | Phantom.prototype.stop = function (callback) { 58 | var self = this; 59 | 60 | if (self.child) { 61 | log.debug('[chimp][phantom] stopping process'); 62 | 63 | var options = { 64 | child: self.child, 65 | prefix: 'phantom' 66 | }; 67 | 68 | processHelper.kill(options, function (err, res) { 69 | self.child = null; 70 | callback(err, res); 71 | }); 72 | 73 | } else { 74 | log.debug('[chimp][phantom] no process to kill'); 75 | callback(null); 76 | } 77 | 78 | }; 79 | 80 | /** 81 | * Interrupt Phantom 82 | * 83 | * @param {Function} callback 84 | * @api public 85 | */ 86 | Phantom.prototype.interrupt = function (callback) { 87 | this.stop(callback); 88 | }; 89 | 90 | module.exports = Phantom; 91 | -------------------------------------------------------------------------------- /src/lib/process-helper.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process'), 2 | log = require('./log'); 3 | 4 | module.exports = { 5 | 6 | start: function (options, callback) { 7 | 8 | var child = this.spawn(options); 9 | 10 | if (options.waitForMessage) { 11 | this.waitForMessage(options, child, callback); 12 | } else { 13 | callback(); 14 | } 15 | 16 | return child; 17 | }, 18 | 19 | spawn: function (options) { 20 | log.debug('[chimp][' + options.prefix + ']', 'starting process'); 21 | 22 | var child = cp.spawn(options.bin, options.args); 23 | 24 | this.logOutputs(options.prefix, child); 25 | 26 | return child; 27 | }, 28 | 29 | logOutputs: function (prefix, child) { 30 | child.stdout.on('data', function (data) { 31 | log.debug('[chimp][' + prefix + '.stdout]', data.toString()); 32 | }); 33 | child.stderr.on('data', function (data) { 34 | log.debug('[chimp][' + prefix + '.stderr]', data.toString()); 35 | }); 36 | }, 37 | 38 | waitForMessage: function (options, child, callback) { 39 | child.stderr.on('data', onDataStdErr); 40 | child.stdout.on('data', onDataStdOut); 41 | 42 | function onDataStdErr(data) { 43 | onData(data, () => { 44 | child.stderr.removeListener('data', onDataStdErr); 45 | }); 46 | } 47 | 48 | function onDataStdOut(data) { 49 | onData(data, () => { 50 | child.stdout.removeListener('data', onDataStdOut); 51 | }); 52 | } 53 | 54 | function onData(data, removeListener) { 55 | if (data.toString().match(options.waitForMessage)) { 56 | removeListener(); 57 | log.debug('[chimp][' + options.prefix + ']', 'started successfully'); 58 | return callback(); 59 | } 60 | if (data.toString().match(options.errorMessage)) { 61 | log.error('[chimp][' + options.prefix + ']', 'failed to start'); 62 | log.error(data.toString()); 63 | callback(data.toString()); 64 | } 65 | } 66 | }, 67 | 68 | kill: function (options, callback) { 69 | 70 | log.debug('[chimp][' + options.prefix + ']', 'kill called on ' + options.prefix + ' process with pid', options.child.pid); 71 | 72 | options.signal = options.signal || 'SIGTERM'; 73 | 74 | try { 75 | log.debug('[chimp][' + options.prefix + ']', 'checking if process exists'); 76 | process.kill(options.child.pid, 0); 77 | log.debug('[chimp][' + options.prefix + ']', options.prefix + ' process exists, killing it with', options.signal); 78 | process.kill(options.child.pid, options.signal); 79 | } catch (e) { 80 | log.debug('[chimp][' + options.prefix + ']', options.prefix + ' process does not exists, ignoring'); 81 | options.child = null; 82 | return callback(); 83 | } 84 | 85 | var delay = 300, 86 | totalRetries = 10, 87 | retries = totalRetries * (1000 / delay), 88 | attempt = 0; 89 | 90 | var waitForProcessToDie = setInterval(function () { 91 | try { 92 | if (retries-- < 0) { 93 | throw new Error('Process took too long to die'); 94 | } 95 | log.debug('[chimp][' + options.prefix + ']', 'waiting for process to die (' + attempt++ + '/' + totalRetries + ')'); 96 | process.kill(options.child.pid, 0); 97 | } catch (e) { 98 | clearInterval(waitForProcessToDie); 99 | if (e.code === 'ESRCH') { 100 | log.debug('[chimp][' + options.prefix + ']', 'process is dead'); 101 | options.child = null; 102 | callback(); 103 | } else { 104 | callback(e); 105 | } 106 | } 107 | }, delay); 108 | } 109 | 110 | }; 111 | -------------------------------------------------------------------------------- /src/lib/saucelabs-manager.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | log = require('./log'), 3 | _ = require('underscore'); 4 | 5 | /** 6 | * SessionManager Constructor 7 | * 8 | * @param {Object} options 9 | * @api public 10 | */ 11 | function SauceLabsSessionManager(options) { 12 | 13 | log.debug('[chimp][saucelabs-session-manager] options are', options); 14 | 15 | var host = options.host.replace('ondemand.', ''); 16 | var sauceLabsBaseUrl = 'https://' + options.user + ':' + options.key + '@' + host + '/rest/v1/' + options.user; 17 | options.sauceLabsUrl = sauceLabsBaseUrl; 18 | 19 | this.options = options; 20 | 21 | this.maxRetries = 30; 22 | this.retryDelay = 3000; 23 | this.retry = 0; 24 | 25 | this.build = null; 26 | 27 | log.debug('[chimp][saucelabs-session-manager] created a new SessionManager', options); 28 | 29 | } 30 | 31 | SauceLabsSessionManager.prototype.webdriver = require('xolvio-sync-webdriverio'); 32 | 33 | /** 34 | * Wraps the webdriver remote method and allows reuse options 35 | * 36 | * @api public 37 | */ 38 | SauceLabsSessionManager.prototype.remote = function (webdriverOptions, callback) { 39 | 40 | var self = this; 41 | 42 | log.debug('[chimp][saucelabs-session-manager] creating webdriver remote '); 43 | var browser = this.webdriver.remote(webdriverOptions); 44 | 45 | this.build = webdriverOptions.desiredCapabilities.build; 46 | log.debug('[chimp][saucelabs-session-manager] build: ' + this.build); 47 | 48 | callback(null, browser); 49 | return; 50 | }; 51 | 52 | var DEFAULT_LIMIT = 100; 53 | 54 | /** 55 | * Gets a list of sessions from the SauceLabs API based on Build ID 56 | * 57 | * @api private 58 | */ 59 | SauceLabsSessionManager.prototype._getJobs = function (callback, limit, skip) { 60 | var hub = this.options.sauceLabsUrl + '/jobs?full=true&limit=' + limit; 61 | if (skip > 0) { 62 | hub += '&skip=' + skip; 63 | } 64 | 65 | log.debug('[chimp][saucelabs-session-manager]', 'requesting sessions from', hub); 66 | 67 | request(hub, function (error, response, body) { 68 | if (!error && response.statusCode === 200) { 69 | log.debug('[chimp][saucelabs-session-manager]', 'received data', body); 70 | callback(null, JSON.parse(body)); 71 | } else { 72 | log.error('[chimp][saucelabs-session-manager]', 'received error', error); 73 | callback(error); 74 | } 75 | }); 76 | }; 77 | 78 | /** 79 | * @api public 80 | */ 81 | SauceLabsSessionManager.prototype.killCurrentSession = function (callback) { 82 | 83 | var self = this; 84 | var killSession = function (job) { 85 | // This will stop the session, causing a 'User terminated' error. 86 | // If we don't manually stop the session, we get a timed-out error. 87 | var options = { 88 | url: this.options.sauceLabsUrl + '/jobs/' + job.id + '/stop', 89 | method: 'PUT' 90 | }; 91 | 92 | request(options, function (error, response) { 93 | if (!error && response.statusCode === 200) { 94 | log.debug('[chimp][saucelabs-session-manager]', 'stopped session'); 95 | callback(); 96 | } else { 97 | log.error('[chimp][saucelabs-session-manager]', 'received error', error); 98 | callback(error); 99 | } 100 | }); 101 | 102 | // This will set the session to passing or else it will show as Errored out 103 | // even though we stop it. 104 | options = { 105 | url: this.options.sauceLabsUrl + '/jobs/' + job.id, 106 | method: 'PUT', 107 | json: true, 108 | body: { passed: true } 109 | }; 110 | 111 | request(options, function (error, response) { 112 | if (!error && response.statusCode === 200) { 113 | log.debug('[chimp][saucelabs-session-manager]', 'updated session to passing state'); 114 | callback(); 115 | } else { 116 | log.error('[chimp][saucelabs-session-manager]', 'received error', error); 117 | callback(error); 118 | } 119 | }); 120 | }.bind(this) 121 | 122 | var getJobForBuild = function(cb, skip) { 123 | if (!self.build) { 124 | // get one and kill it, this is the (flawed) default of chimp which will just randomly 125 | // terminate sessions and you get an odd `Error: The test with session id XXX has already finished, and can't receive further commands.` error 126 | console.warn('You should really consider setting a build, otherwise random sessions will be terminated!'); 127 | this._getJobs(function(err, jobs) { 128 | if (jobs.length) { 129 | killSession(jobs[0]); 130 | } 131 | }, 1); 132 | return; 133 | } 134 | this._getJobs(function(err, jobs) { 135 | // the original code never uses this error, 136 | // probably because if we don't find anything we just leave the session alone 137 | // we might want to revisit this behavior 138 | // if (err) { 139 | // cb(err); 140 | // } 141 | if (!jobs.length) { 142 | // no more jobs found, let's exit 143 | console.warn(`Couldn't find a job to terminate for build ${self.build}`); 144 | callback(); 145 | return; 146 | } 147 | var currentJob = _.find(jobs, function (b) { 148 | return b.build === self.build; 149 | }); 150 | if (!currentJob) { 151 | // maybe it's in the next batch? 152 | getJobForBuild(cb, skip + DEFAULT_LIMIT) 153 | } else { 154 | killSession(currentJob); 155 | } 156 | }, DEFAULT_LIMIT, skip); 157 | }.bind(this) 158 | 159 | 160 | 161 | getJobForBuild(killSession, 0); 162 | }; 163 | 164 | module.exports = SauceLabsSessionManager; 165 | -------------------------------------------------------------------------------- /src/lib/screenshot-helper.js: -------------------------------------------------------------------------------- 1 | const booleanHelper = require('./boolean-helper'); 2 | 3 | const screenshotHelper = { 4 | shouldTakeScreenshot: (status) => { 5 | return booleanHelper.isTruthy(process.env['chimp.captureAllStepScreenshots']) || 6 | ( 7 | status !== 'passed' && 8 | booleanHelper.isTruthy(process.env['chimp.screenshotsOnError']) 9 | ); 10 | }, 11 | 12 | saveScreenshotsToDisk: (fileName, projectDir) => { 13 | if (global.browser.instances) { 14 | global.browser.instances.forEach(function (instance, index) { 15 | instance.captureSync(fileName + '_browser_' + index, projectDir); 16 | }); 17 | } else { 18 | global.browser.captureSync(fileName, projectDir); 19 | } 20 | } 21 | }; 22 | 23 | module.exports = screenshotHelper; 24 | -------------------------------------------------------------------------------- /src/lib/selenium.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'), 2 | processHelper = require('./process-helper.js'), 3 | selenium = require('selenium-standalone'), 4 | booleanHelper = require('./boolean-helper'), 5 | log = require('./log'); 6 | 7 | class Selenium { 8 | /** 9 | * Selenium Constructor 10 | * 11 | * @param {Object} options 12 | * @api public 13 | */ 14 | constructor(options) { 15 | if (!options) { 16 | throw new Error('options is required'); 17 | } 18 | 19 | if (booleanHelper.isFalsey(options.port)) { 20 | throw new Error('options.port is required'); 21 | } 22 | 23 | this.options = _.clone(options); 24 | 25 | this.seleniumStandaloneOptions = options.seleniumStandaloneOptions; 26 | 27 | if (!this.options['clean-selenium-server']) { 28 | // poor-man's singleton is enough for our needs 29 | if (typeof Selenium.instance === 'object') { 30 | log.debug('[chimp][selenium]', 'Selenium object already exists, not creating a new one'); 31 | return Selenium.instance; 32 | } 33 | log.debug('[chimp][selenium]', 'Selenium object created'); 34 | Selenium.instance = this; 35 | } 36 | 37 | this.options.port = String(options.port); 38 | this.child = null; 39 | } 40 | 41 | 42 | /** 43 | * Installs Selenium and drivers. 44 | * 45 | * @param {Function} callback 46 | * @api public 47 | */ 48 | install = (callback) => { 49 | const ProgressBar = require('progress'); 50 | let bar; 51 | let firstProgress = true; 52 | Selenium.installCallback = callback; 53 | 54 | if (this.options.offline) { 55 | log.debug('[chimp][selenium]', 'Offline mode enabled, Chimp will not attempt to install Selenium & Drivers'); 56 | callback(); 57 | return; 58 | } 59 | 60 | log.debug('[chimp][selenium]', 'Installing Selenium + drivers if needed'); 61 | 62 | this.seleniumStandaloneOptions.progressCb = progressCb; 63 | 64 | selenium.install(this.seleniumStandaloneOptions, (e, r) => { 65 | if (e && e.message.match(/Error: getaddrinfo ENOTFOUND/)) { 66 | log.debug('[chimp][selenium]', e.message); 67 | log.info('[chimp][selenium] Detected a connection error in selenium-standalone. Are you offline?'); 68 | log.info('[chimp][selenium] Consider using the --offline option to explicitly skip installing Selenium & drivers.'); 69 | log.info('[chimp][selenium] Attempting to continue...'); 70 | Selenium.installCallback(null, r); 71 | } else { 72 | Selenium.installCallback(e, r); 73 | } 74 | }); 75 | 76 | function progressCb(total, progress, chunk) { 77 | if (firstProgress) { 78 | firstProgress = false; 79 | } 80 | 81 | bar = bar || new ProgressBar( 82 | 'selenium-standalone installation [:bar] :percent :etas', { 83 | total, 84 | complete: '=', 85 | incomplete: ' ', 86 | width: 20, 87 | }); 88 | 89 | bar.tick(chunk); 90 | } 91 | } 92 | 93 | /** 94 | * Start Selenium process. 95 | * 96 | * It also installs Selenium if necessary. 97 | * 98 | * @param {Function} callback 99 | * @api public 100 | */ 101 | start = (callback) => { 102 | const self = this; 103 | const port = self.options.port; 104 | Selenium.startCallback = callback; 105 | 106 | log.debug('[chimp][selenium]', 'Start'); 107 | 108 | if (self.child) { 109 | log.debug('[chimp][selenium]', 'Already running, doing nothing'); 110 | callback(null); 111 | return; 112 | } 113 | 114 | self.install((error) => { 115 | if (error) { 116 | log.error('[selenium]', error); 117 | Selenium.startCallback(error); 118 | return; 119 | } 120 | 121 | if (!self.seleniumStandaloneOptions.seleniumArgs) { 122 | self.seleniumStandaloneOptions.seleniumArgs = []; 123 | } 124 | self.seleniumStandaloneOptions.seleniumArgs.push('-port', port); 125 | 126 | if (process.env['chimp.log'] === 'verbose' || process.env['chimp.log'] === 'debug') { 127 | self.options.seleniumDebug = true; 128 | } 129 | 130 | if (self.options.seleniumDebug) { 131 | self.seleniumStandaloneOptions.seleniumArgs.push('-debug'); 132 | } 133 | 134 | log.debug(`\n[chimp][selenium] hub can be seen at http://localhost:${port}/wd/hub`); 135 | 136 | selenium.start(self.seleniumStandaloneOptions, (error, seleniumChild) => { 137 | self.child = seleniumChild; 138 | 139 | if (error) { 140 | Selenium.startCallback(error); 141 | return; 142 | } 143 | 144 | if (self.options.seleniumDebug) { 145 | processHelper.logOutputs('selenium', self.child); 146 | } 147 | 148 | process.on('exit', () => { 149 | log.debug('[chimp][selenium] process exit event detected. Stopping process'); 150 | self.stop(() => { 151 | log.debug('[chimp][selenium] process exit event stop complete'); 152 | }); 153 | }); 154 | 155 | Selenium.startCallback(null); 156 | }); 157 | }); 158 | } 159 | 160 | stop = (callback) => { 161 | const self = this; 162 | Selenium.stopCallback = callback; 163 | 164 | if (self.child) { 165 | log.debug('[chimp][selenium] killing active session'); 166 | 167 | const options = { 168 | child: self.child, 169 | prefix: 'selenium', 170 | signal: 'SIGINT', 171 | }; 172 | 173 | log.debug('[chimp][selenium] stopping process'); 174 | processHelper.kill(options, (err, res) => { 175 | self.child = null; 176 | Selenium.stopCallback(err, res); 177 | }); 178 | } else { 179 | log.debug('[chimp][selenium] no process to kill'); 180 | Selenium.stopCallback(null); 181 | } 182 | } 183 | 184 | interrupt = (callback) => { 185 | log.debug('[chimp][selenium] interrupt called'); 186 | if (!this.options.watch || !!this.options['clean-selenium-server']) { 187 | this.stop(callback); 188 | } else { 189 | log.debug('[chimp][selenium] interrupt is not killing selenium because ' + 190 | 'clean-selenium-server not set'); 191 | callback(null); 192 | } 193 | } 194 | } 195 | 196 | module.exports = Selenium; 197 | -------------------------------------------------------------------------------- /src/lib/session-factory.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | log = require('./log'); 3 | 4 | var SessionManager = require('./session-manager.js'); 5 | var BsManager = require('./browserstack-manager.js'); 6 | var SlManager = require('./saucelabs-manager.js'); 7 | var TbManager = require('./testingbot-manager.js'); 8 | 9 | /** 10 | * Wraps creation of different Session Managers depending on host value. 11 | * 12 | * @param {Object} options 13 | * @api public 14 | */ 15 | function SessionManagerFactory(options) { 16 | 17 | log.debug('[chimp][session-manager-factory] options are', options); 18 | 19 | if (!options) { 20 | throw new Error('options is required'); 21 | } 22 | 23 | if (!options.port) { 24 | throw new Error('options.port is required'); 25 | } 26 | 27 | if (!options.browser && !options.deviceName) { 28 | throw new Error('[chimp][session-manager-factory] options.browser or options.deviceName is required'); 29 | } 30 | 31 | if (options.host && (options.host.indexOf('browserstack') > -1 || options.host.indexOf('saucelabs') > -1 || options.host.indexOf('testingbot') > -1)) { 32 | 33 | if (!options.user || !options.key) { 34 | throw new Error('[chimp][session-manager-factory] options.user and options.key are required'); 35 | } 36 | 37 | if (options.host.indexOf('browserstack') > -1) { 38 | return new BsManager(options); 39 | } else if (options.host.indexOf('saucelabs') > -1) { 40 | return new SlManager(options); 41 | } else if (options.host.indexOf('testingbot') > -1) { 42 | return new TbManager(options); 43 | } 44 | 45 | } else { 46 | return new SessionManager(options); 47 | } 48 | 49 | } 50 | 51 | module.exports = SessionManagerFactory; 52 | -------------------------------------------------------------------------------- /src/lib/simian-reporter.js: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import log from './log'; 3 | 4 | /** 5 | * SimianReporter Constructor 6 | * 7 | * @param {Object} options 8 | * @api public 9 | */ 10 | function SimianReporter(options) { 11 | this.options = options; 12 | 13 | // FIXME: We need a way to isolate instance in jest tests, until then this allows asserions 14 | SimianReporter.instance = this; 15 | } 16 | 17 | SimianReporter.prototype.report = function report(jsonCucumberResult, callback) { 18 | SimianReporter.reportCallback = callback; 19 | const query = { 20 | accessToken: this.options.simianAccessToken, 21 | }; 22 | if (this.options.simianRepositoryId) { 23 | query.repositoryId = this.options.simianRepositoryId; 24 | } 25 | const url = require('url').format({ 26 | protocol: 'http', 27 | host: this.options.simianResultEndPoint, 28 | query, 29 | }); 30 | 31 | const data = { 32 | type: 'cucumber', 33 | branch: this.options.simianResultBranch, 34 | result: jsonCucumberResult, 35 | }; 36 | if (this.options.simianBuildNumber) { 37 | data.buildNumber = parseInt(this.options.simianBuildNumber, 10); 38 | } 39 | 40 | request.post( 41 | { 42 | url, 43 | json: true, 44 | body: data, 45 | }, 46 | (error, response, body) => { 47 | if (!error && response.statusCode === 200) { 48 | log.debug('[chimp][simian-reporter]', 'received data', body); 49 | } else { 50 | if (body) { 51 | log.error('[chimp][simian-reporter] Error from Simian:', body.error); 52 | } else { 53 | log.error('[chimp][simian-reporter]', 'Error while sending result to Simian:', error); 54 | } 55 | } 56 | SimianReporter.reportCallback(error); 57 | } 58 | ); 59 | }; 60 | 61 | module.exports = SimianReporter; 62 | -------------------------------------------------------------------------------- /src/lib/testingbot-manager.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | log = require('./log'); 3 | 4 | /** 5 | * SessionManager Constructor 6 | * 7 | * @param {Object} options 8 | * @api public 9 | */ 10 | function TestingBotSessionManager(options) { 11 | 12 | log.debug('[chimp][testingbot-session-manager] options are', options); 13 | 14 | var testingbotBaseUrl = 'https://' + options.user + ':' + options.key + '@api.testingbot.com/v1/'; 15 | options.testingbotBaseUrl = testingbotBaseUrl; 16 | 17 | this.options = options; 18 | 19 | this.maxRetries = 30; 20 | this.retryDelay = 3000; 21 | this.retry = 0; 22 | 23 | log.debug('[chimp][testingbot-session-manager] created a new SessionManager', options); 24 | 25 | } 26 | 27 | TestingBotSessionManager.prototype.webdriver = require('xolvio-sync-webdriverio'); 28 | 29 | /** 30 | * Wraps the webdriver remote method and allows reuse options 31 | * 32 | * @api public 33 | */ 34 | TestingBotSessionManager.prototype.remote = function (webdriverOptions, callback) { 35 | 36 | var self = this; 37 | 38 | log.debug('[chimp][testingbot-session-manager] creating webdriver remote '); 39 | var browser = this.webdriver.remote(webdriverOptions); 40 | 41 | callback(null, browser); 42 | return; 43 | }; 44 | 45 | /** 46 | * Gets a list of sessions from the TestingBot API based on Build ID 47 | * 48 | * @api private 49 | */ 50 | TestingBotSessionManager.prototype._getJobs = function (callback) { 51 | var hub = this.options.testingbotBaseUrl + '/tests?count=10'; //default is 100 which seems like too much 52 | 53 | log.debug('[chimp][testingbot-session-manager]', 'requesting sessions from', hub); 54 | 55 | request(hub, function (error, response, body) { 56 | if (!error && response.statusCode === 200) { 57 | log.debug('[chimp][testingbot-session-manager]', 'received data', body); 58 | callback(null, JSON.parse(body)); 59 | } else { 60 | log.error('[chimp][testingbot-session-manager]', 'received error', error); 61 | callback(error); 62 | } 63 | }); 64 | }; 65 | 66 | /** 67 | * Kills the 1st session found running on TestingBot 68 | * 69 | * @api public 70 | */ 71 | TestingBotSessionManager.prototype.killCurrentSession = function (callback) { 72 | 73 | this._getJobs(function (err, jobs) { 74 | if (jobs && jobs.length) { 75 | var job = jobs[0]; 76 | 77 | // This will stop the session, causing a 'User terminated' error. 78 | // If we don't manually stop the session, we get a timed-out error. 79 | var options = { 80 | url: this.options.testingbotBaseUrl + '/tests/' + job.id + '/stop', 81 | method: 'PUT' 82 | }; 83 | 84 | request(options, function (error, response) { 85 | if (!error && response.statusCode === 200) { 86 | log.debug('[chimp][testingbot-session-manager]', 'stopped session'); 87 | callback(); 88 | } else { 89 | log.error('[chimp][testingbot-session-manager]', 'received error', error); 90 | callback(error); 91 | } 92 | }); 93 | 94 | // This will set the session to passing or else it will show as Errored out 95 | // even though we stop it. 96 | options = { 97 | url: this.options.testingbotBaseUrl + '/tests/' + job.id, 98 | method: 'PUT', 99 | json: true, 100 | body: { 'test[passed]': true } 101 | }; 102 | 103 | request(options, function (error, response) { 104 | if (!error && response.statusCode === 200) { 105 | log.debug('[chimp][testingbot-session-manager]', 'updated session to passing state'); 106 | callback(); 107 | } else { 108 | log.error('[chimp][testingbot-session-manager]', 'received error', error); 109 | callback(error); 110 | } 111 | }); 112 | } 113 | }.bind(this)); 114 | }; 115 | 116 | module.exports = TestingBotSessionManager; 117 | -------------------------------------------------------------------------------- /src/lib/utils/escape-reg-exp.js: -------------------------------------------------------------------------------- 1 | export default function escapeRegExp(string) { 2 | return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/utils/fiberize.js: -------------------------------------------------------------------------------- 1 | import Fiber from 'fibers'; 2 | 3 | // Wrap a function in a fiber. 4 | // Correctly handles expected presence of done callback 5 | export function fiberize(fn) { 6 | return function (done) { 7 | const self = this; 8 | Fiber(function () { 9 | if (fn.length === 1) { 10 | fn.call(self, done); 11 | } else { 12 | var res = fn.call(self); 13 | if (typeof res === 'object' && res !== null && typeof res.then === 'function') { 14 | res.then(() => done()).catch(done); 15 | } else { 16 | done(); 17 | } 18 | } 19 | }).run(); 20 | }; 21 | } 22 | 23 | export function fiberizeSync(fn) { 24 | return function () { 25 | const self = this; 26 | Fiber(function () { 27 | fn.call(self); 28 | }).run(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/versions.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'), 2 | fs = require('fs'), 3 | os = require('os'), 4 | log = require('./log'), 5 | async = require('async'), 6 | request = require('request'), 7 | spawnSync = require('child_process').spawnSync, 8 | selenium = require('selenium-standalone'), 9 | chromedriver = require('chromedriver'), 10 | booleanHelper = require('./boolean-helper'), 11 | processHelper = require('./process-helper.js'); 12 | 13 | function Versions(options) { 14 | this.options = options; 15 | 16 | this.appDir = '../..'; 17 | this.chromeDriverExec = chromedriver.path; 18 | 19 | this.show = (callback) => { 20 | console.log('Chimp version: ', this.getChimpVersion()); 21 | console.log('ChromeDriver version: ', this.getChromeDriverVersion()); 22 | console.log('Java version: ', this.getJavaVersion()); 23 | console.log('Selenium version: ', this.getSeleniumVersion()); 24 | console.log('Selenium drivers version: ', this.getSeleniumDriversVersion()); 25 | console.log('OS version: ', this.getOsVersion()); 26 | console.log('Node version: ', this.getNodeVersion()); 27 | this.getCurrentBrowserVersion((browserVersion) => { 28 | const currentBrowser = this.options.browser || 'chrome'; 29 | console.log('Browser version: ', currentBrowser, browserVersion); 30 | if (callback) { 31 | callback(); 32 | } 33 | }); 34 | }; 35 | 36 | this.getChimpVersion = () => { 37 | const packageJson = require(`${this.appDir}/package.json`); 38 | return packageJson.version; 39 | }; 40 | 41 | this.getChromeDriverVersion = () => { 42 | if (booleanHelper.isFalsey(this.options.host)) { 43 | if (booleanHelper.isTruthy(this.options.browser)) { 44 | return 'Unknown. Chromedriver not used directly.'; 45 | } 46 | 47 | return this._spawnSync(`${this.chromeDriverExec} -v`); 48 | } 49 | 50 | return 'Unknown. Selenium host provided.'; 51 | }; 52 | 53 | this.getJavaVersion = () => this._spawnSync('java -version'); 54 | 55 | this.getSeleniumVersion = () => { 56 | if (this.options.seleniumStandaloneOptions) { 57 | return this.options.seleniumStandaloneOptions.version; 58 | } 59 | return 'Unknown.'; 60 | }; 61 | 62 | this.getSeleniumDriversVersion = () => { 63 | if (this.options.seleniumStandaloneOptions) { 64 | const driversVersion = []; 65 | const drivers = this.options.seleniumStandaloneOptions.drivers; 66 | _.each(_.keys(drivers), (driverName) => { 67 | driversVersion.push(`${driverName}: ${drivers[driverName].version}`); 68 | }); 69 | return driversVersion.toString().replace(/,/g, ', '); 70 | } 71 | return 'Unknown.'; 72 | }; 73 | 74 | this.getOsVersion = () => `${os.type()} ${os.release()}`; 75 | 76 | this.getNodeVersion = () => process.version; 77 | 78 | this.getCurrentBrowserVersion = (callback) => { 79 | if (booleanHelper.isTruthy(this.options.browser)) { 80 | const seleniumOptions = _.clone(this.options.seleniumStandaloneOptions); 81 | seleniumOptions.port = 1; 82 | 83 | async.series([ 84 | (cb) => { 85 | selenium.install( 86 | seleniumOptions 87 | , (err, seleniumInstallPaths) => { 88 | cb(err, seleniumInstallPaths); 89 | }); 90 | }, 91 | ], 92 | (err, seleniumInstallPaths) => { 93 | const selectedBrowserDriver = seleniumInstallPaths[0][this.options.browser]; 94 | if (selectedBrowserDriver) { 95 | const startBrowserOptions = { 96 | path: selectedBrowserDriver.installPath, 97 | port: this.options.port, 98 | }; 99 | this._startBrowserDriver(startBrowserOptions, () => { 100 | this._getBrowserVersion(startBrowserOptions, (err, browserVersion) => { 101 | this._stopBrowserDriver((err) => { 102 | if (err) { 103 | log.warn(err); 104 | } 105 | callback(browserVersion); 106 | }); 107 | }); 108 | }); 109 | } else { 110 | callback(`Driver for selected browser(${this.options.browser}) not found.`); 111 | } 112 | }, 113 | ); 114 | } else if (fs.existsSync(this.chromeDriverExec)) { 115 | const startBrowserOptions = { 116 | path: this.chromeDriverExec, 117 | port: this.options.port, 118 | }; 119 | this._startBrowserDriver(startBrowserOptions, () => { 120 | this._getBrowserVersion(startBrowserOptions, (err, browserVersion) => { 121 | this._stopBrowserDriver((err) => { 122 | if (err) { 123 | log.warn(err); 124 | } 125 | callback(browserVersion); 126 | }); 127 | }); 128 | }); 129 | } else { 130 | callback('Driver for selected browser not found.'); 131 | } 132 | }; 133 | 134 | // ------------------------------------------------------------------------------------- 135 | 136 | this._startBrowserDriver = (options, callback) => { 137 | const waitMessage = new RegExp(`${options.port}`); 138 | this.child = processHelper.start({ 139 | bin: options.path, 140 | prefix: 'browserdriver', 141 | args: [`--port=${options.port}`], 142 | waitForMessage: waitMessage, 143 | errorMessage: /Error/, 144 | }, callback); 145 | }; 146 | 147 | this._getBrowserVersion = (options, callback) => { 148 | const url = `http://localhost:${options.port}/session`; 149 | const data = {desiredCapabilities: {}}; 150 | 151 | request.post( 152 | { 153 | url, 154 | json: true, 155 | body: data, 156 | }, 157 | (error, response, body) => { 158 | const data = {}; 159 | if (!error && response.statusCode === 200) { 160 | data.sessionId = body.sessionId; 161 | data.browserVersion = body.value.version; 162 | request.delete(`${url}/${data.sessionId}`, 163 | () => { 164 | callback(null, data.browserVersion); 165 | }, 166 | ); 167 | } else { 168 | error = 'Error connecting to browser driver.'; 169 | callback(error); 170 | } 171 | }, 172 | ); 173 | }; 174 | 175 | this._stopBrowserDriver = (callback) => { 176 | if (this.child) { 177 | const options = { 178 | child: this.child, 179 | prefix: 'browserdriver', 180 | }; 181 | 182 | processHelper.kill(options, (err, res) => { 183 | this.child = null; 184 | callback(err, res); 185 | }); 186 | } else { 187 | callback(null); 188 | } 189 | }; 190 | 191 | this._spawnSync = (commandToRun) => { 192 | const endLine = new RegExp(`${os.EOL}`, 'g'); 193 | const commandOptions = commandToRun.split(' '); 194 | const command = commandOptions.shift(); 195 | const commandResult = spawnSync(command, commandOptions); 196 | if (commandResult.status !== 0 && commandResult.error) { 197 | if (commandResult.error.code === 'ENOENT') { 198 | return 'No such file or directory'; 199 | } 200 | 201 | return `Error ${commandResult.error.code}`; 202 | } 203 | 204 | let commandToReturn = ''; 205 | _.each(commandResult.output, (output) => { 206 | if (output && output.length) { 207 | commandToReturn += output.toString().trim(); 208 | } 209 | }); 210 | return commandToReturn.replace(endLine, ', '); 211 | }; 212 | } 213 | 214 | module.exports = Versions; 215 | -------------------------------------------------------------------------------- /tests/.babelignore: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /tests/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-transform-runtime"], 3 | "presets": ["@babel/env"] 4 | } 5 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "jest": false, 4 | "jasmine": false, 5 | "spyOn": false, 6 | "it": false, 7 | "describe": false, 8 | "expect": false, 9 | "after": false, 10 | "before": false, 11 | "beforeAll": false, 12 | "beforeEach": false, 13 | "afterAll": false, 14 | "afterEach": false, 15 | "xit": false, 16 | "xdescribe": false, 17 | "fit": false, 18 | "fdescribe": false, 19 | "pending": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/cucumber/features/globalPending.feature: -------------------------------------------------------------------------------- 1 | Feature: Global pending 2 | Scenario: User uses global pending 3 | Given global pending is defined -------------------------------------------------------------------------------- /tests/cucumber/features/step_definitions/Given_global_pending_is_defined.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | this.Given(/^global pending is defined$/, function() { 3 | pending(); 4 | }); 5 | }; -------------------------------------------------------------------------------- /tests/jasmine/jasmine-spec.js: -------------------------------------------------------------------------------- 1 | describe('Chimp Jasmine', () => { 2 | describe('Page title', () => { 3 | it('should be set by the Meteor method', () => { 4 | browser.url('http://google.com'); 5 | expect(browser.getTitle()).toBe('Google'); 6 | }); 7 | }); 8 | 9 | describe('ES2015', function () { 10 | it('is supported', function () { 11 | const {a, b} = {a: 'foo', b: 'bar'}; 12 | const arrowFunc = (foo) => foo; 13 | class Foo { 14 | constructor() {} 15 | foo() {} 16 | } 17 | var object = { 18 | foo() {} 19 | }; 20 | const templateString = `Foo`; 21 | const [c, ,d] = [1,2,3]; 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/mocha/mocha-spec.js: -------------------------------------------------------------------------------- 1 | describe('Chimp Mocha', () => { 2 | describe('Page title', () => { 3 | it('should be set by the Meteor method @watch', () => { 4 | browser.url('http://google.com'); 5 | expect(browser.getTitle()).to.equal('Google'); 6 | }); 7 | }); 8 | 9 | describe('ES2015', function () { 10 | it('is supported', function () { 11 | const {a, b} = {a: 'foo', b: 'bar'}; 12 | const arrowFunc = (foo) => foo; 13 | class Foo { 14 | constructor() {} 15 | foo() {} 16 | } 17 | var object = { 18 | foo() {} 19 | }; 20 | const templateString = `Foo`; 21 | const [c, ,d] = [1,2,3]; 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /wallaby-mocha.js: -------------------------------------------------------------------------------- 1 | module.exports = (wallaby) => { 2 | const path = require('path'); 3 | const packageJson = require(`${wallaby.localProjectDir}/package.json`); 4 | return { 5 | debug: false, 6 | testFramework: 'mocha', 7 | files: packageJson.mocha.files, 8 | tests: packageJson.mocha.tests, 9 | compilers: {'**/*.js': wallaby.compilers.babel()}, 10 | env: {type: 'node'}, 11 | workers: {initial: 1, regular: 1, recycle: true}, 12 | setup: () => { 13 | wallaby.testFramework.addFile(`${wallaby.localProjectDir}/mocha-setup.js`); 14 | }, 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var babel = require('babel-core'); 6 | var babelConfig = JSON.parse( 7 | fs.readFileSync(path.join(__dirname, '.babelrc')) 8 | ); 9 | babelConfig.babel = babel; 10 | 11 | return { 12 | files: [ 13 | 'src/bin/*.js', 14 | 'src/lib/**/*.js', 15 | 'src/__mocks__/*.js' 16 | ], 17 | 18 | tests: [ 19 | 'src/__tests__/*.js' 20 | ], 21 | 22 | compilers: { 23 | '**/*.js': wallaby.compilers.babel(babelConfig) 24 | }, 25 | 26 | env: { 27 | type: 'node', 28 | params: { 29 | runner: '--harmony' 30 | } 31 | }, 32 | 33 | testFramework: 'jest', 34 | 35 | bootstrap: function (wallaby) { 36 | var path = require('path'); 37 | var packageConfigPath = path.resolve(wallaby.localProjectDir, 'package.json'); 38 | var packageConfig = require(packageConfigPath); 39 | var jestConfig = packageConfig.jest; 40 | delete jestConfig.scriptPreprocessor; 41 | wallaby.testFramework.configure(jestConfig); 42 | }, 43 | 44 | debug: true 45 | }; 46 | }; 47 | --------------------------------------------------------------------------------