├── .gitignore ├── .jscsrc ├── .travis.yml ├── docs └── api.md ├── lib └── index.js ├── package.json ├── readme.md ├── test ├── analysis │ └── gc-thrash.js ├── examples │ ├── inputs.html │ └── leaking.html └── tests.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maximumLineLength": 120 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | env: DISPLAY=':99.0' 4 | addons: 5 | apt: 6 | sources: 7 | - google-chrome 8 | packages: 9 | - google-chrome-stable 10 | node_js: 11 | - "6.9.0" 12 | before_install: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | - curl -Lo chromedriver.zip http://chromedriver.storage.googleapis.com/2.31/chromedriver_linux64.zip && unzip chromedriver.zip 16 | script: 17 | - npm test 18 | - npm lint-test 19 | - node test/analysis/gc-thrash.js 20 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Drool API Guide 2 | 3 | Drool's goal is to make it simple to add memory regression and profiling tests to your application with minimal boilerplate. 4 | 5 | #### Table of Contents 6 | 7 | * [Quick Start](#quick-start) 8 | * [API](#api) 9 | * [start](#start) 10 | * [flow](#flow) 11 | * [flow cycle](#flow-cycle) 12 | * [getCounts](#getcounts) 13 | * [webdriver](#webdriver) 14 | 15 | ### Quick Start 16 | 17 | To get started with drool: 18 | 19 | npm i drool assert --save-dev 20 | 21 | The next step is to require and [start](#start) drool. 22 | 23 | ```js 24 | var drool = require('drool'); 25 | 26 | var driver = drool.start({ 27 | chromeOptions: 'no-sandbox', 28 | chromeBinaryPath: '' 29 | }); 30 | ``` 31 | 32 | The next step is to define a flow. A [flow](#flow) is a declarative hash where you define actions at given points in the lifecycle of your drool tests. 33 | 34 | ```js 35 | var res = drool.flow({ 36 | setup: function() { 37 | driver.get('file://' + path.join(__dirname, 'examples/', 'leaking.html')); 38 | }, 39 | action: function() { 40 | driver.findElement(webdriver.By.css('#leak')).click(); 41 | }, 42 | assert: function(after, initial) { 43 | assert.notEqual(initial.counts.nodes, after.counts.nodes, 'node count should not match'); 44 | } 45 | }, driver); 46 | ``` 47 | 48 | Once you are done interacting with the driver, you then will want to quit the driver (to close the browser). 49 | 50 | ```js 51 | res 52 | .then(() => driver.quit()) 53 | .catch(e => { 54 | driver.quit(); 55 | throw e; 56 | }) 57 | ``` 58 | 59 | ### API 60 | 61 | #### start 62 | 63 | `start` returns a [selenium webdriver](http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index.html) instance. It takes an optional argument object that can have two keys, `chromeBinaryPath` and `chromeOptions`. 64 | 65 | * `chromeBinaryPath` must be a string. 66 | * `chromeOptions` can be an array or an array or strings. 67 | 68 | ```js 69 | drool.start({ 70 | chromeBinaryPath: 'my/path/to-chrome', 71 | chromeOptions: ['--foo=1', '--bar=2'] 72 | }); 73 | ``` 74 | 75 | #### flow 76 | 77 | The `flow` method returns a Promise, that will be resolved (or rejected) after the exit step has been called (regardless of if you pass an exit method). Flow takes two required aguments, the first agument is an object that contains the flow actions, the second argument is a [selenium webdriver](http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index.html) instance (For instance the one returned by [start](#start) 78 | 79 | ##### flow cycle 80 | 81 | the "flow" action object is a set of life cycle key value pairs that will be invoked in the following order. 82 | 83 | 1. `setup` 84 | 2. `action` * `prewarmRepeatCount` (to prewarm any DOM/Listener cache) 85 | 3. Initial Measurement is taken after via [getCounts](#getcounts) 86 | 4. `afterPrewarm`, e.g. `() => driver.sleep(1000)` if it worth pausing before getting initial counts 87 | 5. `action` * `repeatCount` times (repeat count defaults to 5) 88 | 6. `beforeAssert` 89 | 7. Final Measurement is taken after via [getCounts](#getcounts) 90 | 8. `exit` 91 | 9. `assert` 92 | 93 | Each step in the flow, **except for assert**, is optional. Keep in mind however that your flow should cleanly `exit`, and `action` should be able to be invoked an unlimited number of times. 94 | 95 | As the invokee of a flow, you can control how many times the action is invoked via `repeatCount`. Increasing this number will allow you to more easily identify a leaking interaction at the cost of time to run. 96 | 97 | For example: 98 | 99 | ```js 100 | return drool.flow({ 101 | repeatCount: 100, 102 | setup: function() { 103 | driver.get('file://' + path.join(__dirname, 'examples/', 'inputs.html')); 104 | }, 105 | action: function() { 106 | driver.findElement(drool.webdriver.By.css('button')).click(); 107 | }, 108 | beforeAssert: function() { 109 | driver.findElement(drool.webdriver.By.css('#clean')).click(); 110 | }, 111 | assert: function(after, initial) { 112 | assert.equal(initial.counts.nodes, after.counts.nodes, 'node count should match'); 113 | }, 114 | exit: function() { 115 | driver.get('https://google.com/'); 116 | } 117 | }, driver); 118 | ``` 119 | 120 | #### getCounts 121 | 122 | The `getCounts` method abstracts the work of forcing a garbage collection and collecting the last performance counter stats from the driver instance. 123 | 124 | `getCounts` returns a promise that resolves with the following data structure: 125 | 126 | ```json 127 | {"counts": { 128 | "documents": 1, 129 | "jsEventListeners": 0, 130 | "jsHeapSizeUsed": 1747160, 131 | "nodes": 5 132 | }, "gc": { 133 | "MajorGC": { 134 | "duration": 1, 135 | "count": 1 136 | }, 137 | "MinorGC": { 138 | "duration": 1, 139 | "count": 1 140 | }, 141 | "V8.GCScavenger": { 142 | "duration": 1, 143 | "count": 1 144 | }, 145 | "V8.GCIncrementalMarking": { 146 | "duration": 1, 147 | "count": 1 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | For example: 154 | 155 | ```js 156 | drool.getCounts(driver).then(function(data) { 157 | console.log('the node count is ' + data.counts.nodes); 158 | }); 159 | ``` 160 | 161 | ### webdriver 162 | 163 | The webdriver object exposed on drool, is a reference to the selenium-webdriver module required and used by drool. 164 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var webdriver = require("selenium-webdriver"); 2 | var chrome = require("selenium-webdriver/chrome"); 3 | var controlFlow = webdriver.promise.controlFlow(); 4 | var driverErrorMessage = 5 | "Please provide a driver (as returned" + 6 | " by drool.start) as the second argument to drool.flow"; 7 | 8 | function executeInFlow(fn) { 9 | if (typeof fn === "function") { 10 | 11 | return controlFlow.execute(fn); 12 | } 13 | 14 | return controlFlow.execute(function() {}); 15 | } 16 | 17 | function getCounts(driver, prev) { 18 | return getLastCounts(driver, prev).then(function(lastCounts) { 19 | return driver.executeScript("gc()").then(function() { 20 | if (!prev) { 21 | console.log("requerying counts to make sure they make sense"); 22 | return getCounts(driver, lastCounts); 23 | } else if (!prev.counts) { 24 | console.log("no counts in previous logs - requerying"); 25 | return getCounts(driver, lastCounts); 26 | } else if (!lastCounts.counts) { 27 | console.log("no counts in logs - requerying"); 28 | return getCounts(driver, prev); 29 | } else if ( 30 | lastCounts.counts.jsHeapSizeUsed < prev.counts.jsHeapSizeUsed 31 | ) { 32 | console.log("heap is still decreazing - requerying after delay", [ 33 | lastCounts.counts.jsHeapSizeUsed, 34 | prev.counts.jsHeapSizeUsed 35 | ]); 36 | return driver.sleep(100).then(() => getCounts(driver, lastCounts)); 37 | } 38 | console.log("counts are allright"); 39 | return lastCounts; 40 | }); 41 | }); 42 | } 43 | 44 | function getLastCounts(driver, last) { 45 | last = last || {}; 46 | 47 | return driver 48 | .manage() 49 | .logs() 50 | .get("performance") 51 | .then(function(performanceLogs) { 52 | last.gc = 53 | last.gc === undefined ? sumGcCounts(performanceLogs, last.gc) : last.gc; 54 | var d = performanceLogs 55 | .filter(function(v) { 56 | return JSON.parse(v.message).message.params.name === "UpdateCounters"; 57 | }) 58 | .pop(); 59 | 60 | var counts = d ? JSON.parse(d.message).message.params.args.data : null; 61 | 62 | return { 63 | counts: counts, 64 | gc: last.gc 65 | }; 66 | }); 67 | } 68 | 69 | function sumGcCounts(traces, last) { 70 | last = last || { 71 | MinorGC: { 72 | count: 0, 73 | duration: 0 74 | }, 75 | MajorGC: { 76 | count: 0, 77 | duration: 0 78 | }, 79 | "V8.GCScavenger": { 80 | count: 0, 81 | duration: 0 82 | }, 83 | "V8.GCIncrementalMarking": { 84 | count: 0, 85 | duration: 0 86 | } 87 | }; 88 | 89 | return traces.reduce(function(prev, val) { 90 | var params = JSON.parse(val.message).message.params; 91 | var name = params.name; 92 | 93 | ["V8.GCScavenger", "V8.GCIncrementalMarking", "MajorGC", "MinorGC"].forEach( 94 | function(v) { 95 | if (name === v) { 96 | if (params.ph === "B") { 97 | prev[v]._start = params.ts; 98 | } else { 99 | if (prev[v]._start) { 100 | prev[v].count++; 101 | prev[v].duration += params.ts - prev[v]._start; 102 | delete prev[v]._start; 103 | } 104 | } 105 | } 106 | } 107 | ); 108 | 109 | return prev; 110 | }, last); 111 | } 112 | 113 | function flow(set, driver) { 114 | var initialCounts; 115 | if (!driver) { 116 | throw new Error(driverErrorMessage); 117 | } 118 | set = Object.assign({ repeatCount: 5, prewarmRepeatCount: 1 }, set); 119 | 120 | executeInFlow(set.setup); 121 | 122 | // prewarm the cache 123 | for (var i = 0; i < set.prewarmRepeatCount; ++i) { 124 | executeInFlow(set.action); 125 | } 126 | 127 | executeInFlow(set.afterPrewarm).then(() => { 128 | getCounts(driver).then(data => (initialCounts = data)); 129 | }); 130 | 131 | for (var i = 0; i < set.repeatCount; ++i) { 132 | executeInFlow(set.action); 133 | } 134 | 135 | var afterCounts; 136 | 137 | executeInFlow(set.beforeAssert) 138 | .then(() => { 139 | return getCounts(driver); 140 | }) 141 | .then(data => (afterCounts = data)) 142 | .then(function() { 143 | return executeInFlow(set.exit); 144 | }) 145 | .then(function() { 146 | set.assert(afterCounts, initialCounts); 147 | }); 148 | } 149 | 150 | function start(opts) { 151 | opts = opts || {}; 152 | 153 | var options = new chrome.Options(); 154 | 155 | if (typeof opts.chromeBinaryPath !== "undefined") { 156 | options.setChromeBinaryPath(opts.chromeBinaryPath); 157 | } 158 | 159 | ["--js-flags=--expose-gc"] 160 | .concat(opts.chromeOptions || []) 161 | .forEach(function(v) { 162 | options.addArguments(v); 163 | }); 164 | 165 | options.setLoggingPrefs({ performance: "ALL" }); 166 | options.setPerfLoggingPrefs({ 167 | traceCategories: "v8,blink.console,disabled-by-default-devtools.timeline" 168 | //jscs:disable 169 | //Fix found here https://github.com/cabbiepete/browser-perf/commit/046f65f02db418c17ec2d59c43abcc0de642a60f 170 | // related to bug https://code.google.com/p/chromium/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Pri%20M%20Week%20ReleaseBlock%20Cr%20Status%20Owner%20Summary%20OS%20Modified&groupby=&sort=&id=474667 171 | //enableTimeline: true 172 | //jscs:enable 173 | }); 174 | 175 | return new webdriver.Builder() 176 | .forBrowser("chrome") 177 | .setChromeOptions(options) 178 | .build(); 179 | } 180 | 181 | module.exports = { 182 | start: start, 183 | flow: flow, 184 | getCounts: getCounts, 185 | webdriver: webdriver 186 | }; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "lib/index.js", 3 | "name": "drool", 4 | "version": "0.6.0", 5 | "scripts": { 6 | "test": "mocha test/tests.js --timeout=10000", 7 | "lint-test": "prettier --list-different" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/samccone/drool.git" 12 | }, 13 | "dependencies": { 14 | "selenium-webdriver": "^3.5.0" 15 | }, 16 | "devDependencies": { 17 | "mocha": "^2.2.5", 18 | "prettier": "^1.10.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 🤤 Drool is an automation layer that is used to measure if a set of "clean" actions results in a DOM and or Listener leak. 2 | 3 |

4 | View the API Docs

5 | Build Status 6 | Join the chat at https://gitter.im/samccone/drool 7 |

8 | 9 | -------------- 10 | 11 | #### Real World wins 12 | 13 | Drool has made it far easier to identify memory leaks in an automated and reproducible way, for example: 14 | 15 | * TodoMVC 16 | * [JSBlocks](https://github.com/tastejs/todomvc/pull/1297#issuecomment-112828342) 17 | * [Atma.js](https://github.com/tastejs/todomvc/pull/1337#issuecomment-112821596) 18 | * [Automated leak based CI failures](https://github.com/tastejs/todomvc/pull/1464) 19 | * Chromium 20 | * [Core input element node leak](https://code.google.com/p/chromium/issues/detail?id=516153) 21 | * Material Design Lite 22 | * [Menu Component Leaking Listeners](https://github.com/google/material-design-lite/issues/761) 23 | * Beaker Notebook 24 | * [CI memory smoketest on every PR](https://github.com/twosigma/beaker-notebook/blob/9298ccf33e646638f8a588405fa8fa5919742636/test/memory-tests.js) 25 | 26 | 27 | ##### Why am I making this? 28 | 29 | After running perf/memory tests across multiple [todomvc](https://github.com/tastejs/todomvc) implementations, I found that almost all implementations have significant memory leaks on the most basic of tasks. Worse yet, most of these leaks were introduced at a framework level, or were introduced by "expert/(framework authors)". The question arose in my mind, if people who authored a framework are introducing leaks in the most trivial of applications, how can users be expected to create non-leaking implementations of much more complex applications. 30 | 31 | ##### Goals 32 | 33 | Ideally Drool will leverage standard interfaces, such as todomvc, to test for leaks at a framework level. The result of which should help framework authors and developers realize that memory leaks are pervasive in the tools that we use. 34 | 35 | Chrome devtools is a powerful utility layer for detecting memory issues, yet the fact still stands that most developers do not know how to use the tooling around it to arrive any thing that is directly actionable. Drool aims to be a generic automated abstraction layer, so people can get good "numbers" in a consistent way without having to deep dive into memory profiling. 36 | 37 | ##### Running 38 | 39 | Ensure that you have at least version `2.26.436421` of chromedriver. 40 | 41 | ```js 42 | var drool = require('drool'); 43 | var assert = require('assert'); 44 | 45 | var driver = drool.start({ 46 | chromeOptions: 'no-sandbox' 47 | }); 48 | 49 | drool.flow({ 50 | repeatCount: 100, 51 | setup: function() { 52 | driver.get('http://todomvc.com/examples/backbone/'); 53 | }, 54 | action: function() { 55 | driver.findElement(drool.webdriver.By.css('.new-todo')).sendKeys('find magical goats', drool.webdriver.Key.ENTER); 56 | driver.findElement(drool.webdriver.By.css('.todo-list li')).click(); 57 | driver.findElement(drool.webdriver.By.css('.destroy')).click(); 58 | }, 59 | assert: function(after, initial) { 60 | assert.equal(initial.counts.nodes, after.counts.nodes, 'node count should match'); 61 | } 62 | }, driver) 63 | .then(() => driver.quit()) 64 | .catch(e => { 65 | driver.quit(); 66 | throw e; 67 | }) 68 | 69 | ``` 70 | 71 | [View the API Docs](docs/api.md) 72 | -------------------------------------------------------------------------------- /test/analysis/gc-thrash.js: -------------------------------------------------------------------------------- 1 | const drool = require('../../'), 2 | By = drool.webdriver.By, 3 | until = drool.webdriver.until, 4 | Key = drool.webdriver.Key; 5 | 6 | var tests = [ 7 | { 8 | url: 'http://todomvc.com/examples/emberjs/index.html', 9 | todo: '#new-todo', 10 | li: '#todo-list li', 11 | destroy: '.destroy' 12 | }, 13 | { 14 | url: 'http://todomvc.com/examples/angularjs/index.html', 15 | todo: '#new-todo', 16 | li: '#todo-list li', 17 | destroy: '.destroy' 18 | }, 19 | { 20 | url: 'http://todomvc.com/examples/vue/index.html', 21 | todo: '.new-todo', 22 | li: '.todo-list li', 23 | destroy: '.destroy' 24 | }, 25 | { 26 | url: 'http://todomvc.com/examples/backbone/index.html', 27 | todo: '.new-todo', 28 | li: '.todo-list li', 29 | destroy: '.destroy' 30 | }, 31 | { 32 | url: 'http://todomvc.com/examples/backbone_marionette/index.html', 33 | todo: '#new-todo', 34 | li: '#todo-list li', 35 | destroy: '.destroy' 36 | }, 37 | { 38 | url: 'http://todomvc.com/examples/jquery/index.html', 39 | todo: '#new-todo', 40 | li: '#todo-list li', 41 | destroy: '.destroy' 42 | }, 43 | { 44 | url: 'http://todomvc.com/examples/ampersand/index.html', 45 | todo: '#new-todo', 46 | li: '#todo-list li', 47 | destroy: '.destroy' 48 | }, 49 | { 50 | url: 'http://todomvc.com/examples/polymer/index.html', 51 | todo: '#new-todo', 52 | li: '#todo-list li', 53 | destroy: '.destroy' 54 | }, 55 | { 56 | url: 'http://todomvc.com/examples/vanillajs/index.html', 57 | todo: '.new-todo', 58 | li: '.todo-list li', 59 | destroy: '.destroy' 60 | }, 61 | { 62 | url: 'http://todomvc.com/examples/react/index.html', 63 | todo: '.new-todo', 64 | li: '.todo-list li', 65 | destroy: '.destroy' 66 | }, 67 | ]; 68 | 69 | tests.forEach(function(test) { 70 | var config = { 71 | chromeOptions: 'no-sandbox' 72 | }; 73 | 74 | if (typeof process.env.chromeBinaryPath !== 'undefined') { 75 | config.chromeBinaryPath = process.env.chromeBinaryPath; 76 | } 77 | 78 | var driver = drool.start(config); 79 | 80 | drool.flow({ 81 | repeatCount: 10, 82 | setup: function() { 83 | driver.get(test.url); 84 | }, 85 | action: function() { 86 | driver.wait(until.elementLocated(By.css(test.todo)), 10000); 87 | 88 | driver.findElement(By.css(test.todo)).sendKeys('find magical goats', Key.ENTER); 89 | 90 | driver.wait(until.elementLocated(By.css(test.li)), 1000); 91 | 92 | driver.findElement(By.css(test.li)).click(); 93 | 94 | driver.wait(until.elementLocated(By.css(test.destroy)), 1000); 95 | 96 | driver.findElement(By.css(test.destroy)).click(); 97 | }, 98 | assert: function(after, initial) { 99 | console.log(test.url, (after.gc.MinorGC.duration + 100 | after.gc.MajorGC.duration + 101 | after.gc['V8.GCScavenger'].duration + 102 | after.gc['V8.GCIncrementalMarking'].duration) + ' μs'); 103 | } 104 | }, driver); 105 | 106 | driver.quit(); 107 | }); 108 | -------------------------------------------------------------------------------- /test/examples/inputs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/examples/leaking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var drool = require('../lib/'); 3 | var path = require('path'); 4 | var config = { 5 | chromeOptions: 'no-sandbox' 6 | }; 7 | 8 | describe('memory tests', function() { 9 | beforeEach(function() { 10 | if (typeof process.env.chromeBinaryPath !== 'undefined') { 11 | config.chromeBinaryPath = process.env.chromeBinaryPath; 12 | } 13 | 14 | this.results = []; 15 | this.driver = drool.start(config); 16 | }); 17 | 18 | it('inputs should not leak when added and removed', function() { 19 | var self = this; 20 | 21 | return drool.flow({ 22 | setup: function() { 23 | self.driver.get('file://' + path.join(__dirname, 'examples/', 'inputs.html')); 24 | }, 25 | action: function() { 26 | self.driver.findElement(drool.webdriver.By.css('#add-remove')).click(); 27 | }, 28 | assert: function(after, initial) { 29 | assert.equal(initial.counts.nodes, after.counts.nodes, 'node count should match'); 30 | } 31 | }, self.driver); 32 | }); 33 | 34 | it('shows leaks', function() { 35 | var self = this; 36 | 37 | return drool.flow({ 38 | setup: function() { 39 | self.driver.get('file://' + path.join(__dirname, 'examples/', 'leaking.html')); 40 | }, 41 | action: function() { 42 | self.driver.findElement(drool.webdriver.By.css('#leak')).click(); 43 | }, 44 | assert: function(after, initial) { 45 | assert.notEqual(initial.counts.nodes, after.counts.nodes, 'node count should not match'); 46 | } 47 | }, self.driver); 48 | }); 49 | 50 | it('gcs correctly', function() { 51 | var self = this; 52 | 53 | return drool.flow({ 54 | setup: function() { 55 | self.driver.get('file://' + path.join(__dirname, 'examples/', 'leaking.html')); 56 | }, 57 | action: function() { 58 | self.driver.findElement(drool.webdriver.By.css('#leak')).click(); 59 | }, 60 | beforeAssert: function() { 61 | self.driver.findElement(drool.webdriver.By.css('#clean')).click(); 62 | }, 63 | assert: function(after, initial) { 64 | // This is a hack, ony because we want to test clearing leaks 65 | // and since the action is run once to prewarm the cache 66 | assert.equal(initial.counts.nodes - 1, after.counts.nodes, 'node count does not grow'); 67 | } 68 | }, self.driver); 69 | }); 70 | 71 | it('exposes gc info', function() { 72 | var self = this; 73 | 74 | return drool.flow({ 75 | setup: function() { 76 | self.driver.get('file://' + path.join(__dirname, 'examples/', 'inputs.html')); 77 | }, 78 | action: function() { 79 | }, 80 | assert: function(after, initial) { 81 | var gcKeys = 'MinorGC,MajorGC,V8.GCScavenger,V8.GCIncrementalMarking'; 82 | 83 | assert.equal(Object.keys(initial.gc).join(','), gcKeys); 84 | assert.equal(Object.keys(after.gc).join(','), gcKeys); 85 | } 86 | }, self.driver); 87 | }); 88 | }); 89 | 90 | describe('drool.flow', function() { 91 | it('throws helpfully when no driver is given', function(done) { 92 | try { 93 | drool.flow({ 94 | setup: function() {}, 95 | action: function() {}, 96 | beforeAssert: function() {}, 97 | assert: function() {} 98 | }); 99 | } catch (e) { 100 | assert.equal( 101 | e.message, 102 | 'Please provide a driver (as returned by drool.start) as the second argument to drool.flow', 103 | 'helpful error is not thrown' 104 | ); 105 | done(); 106 | } 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | balanced-match@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 8 | 9 | brace-expansion@^1.1.7: 10 | version "1.1.8" 11 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 12 | dependencies: 13 | balanced-match "^1.0.0" 14 | concat-map "0.0.1" 15 | 16 | commander@0.6.1: 17 | version "0.6.1" 18 | resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" 19 | 20 | commander@2.3.0: 21 | version "2.3.0" 22 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" 23 | 24 | concat-map@0.0.1: 25 | version "0.0.1" 26 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 27 | 28 | core-js@~2.3.0: 29 | version "2.3.0" 30 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" 31 | 32 | core-util-is@~1.0.0: 33 | version "1.0.2" 34 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 35 | 36 | debug@2.2.0: 37 | version "2.2.0" 38 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 39 | dependencies: 40 | ms "0.7.1" 41 | 42 | diff@1.4.0: 43 | version "1.4.0" 44 | resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" 45 | 46 | es6-promise@~3.0.2: 47 | version "3.0.2" 48 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" 49 | 50 | escape-string-regexp@1.0.2: 51 | version "1.0.2" 52 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" 53 | 54 | fs.realpath@^1.0.0: 55 | version "1.0.0" 56 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 57 | 58 | glob@3.2.11: 59 | version "3.2.11" 60 | resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" 61 | dependencies: 62 | inherits "2" 63 | minimatch "0.3" 64 | 65 | glob@^7.0.5: 66 | version "7.1.2" 67 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 68 | dependencies: 69 | fs.realpath "^1.0.0" 70 | inflight "^1.0.4" 71 | inherits "2" 72 | minimatch "^3.0.4" 73 | once "^1.3.0" 74 | path-is-absolute "^1.0.0" 75 | 76 | growl@1.9.2: 77 | version "1.9.2" 78 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" 79 | 80 | immediate@~3.0.5: 81 | version "3.0.6" 82 | resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" 83 | 84 | inflight@^1.0.4: 85 | version "1.0.6" 86 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 87 | dependencies: 88 | once "^1.3.0" 89 | wrappy "1" 90 | 91 | inherits@2, inherits@~2.0.1: 92 | version "2.0.3" 93 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 94 | 95 | isarray@~1.0.0: 96 | version "1.0.0" 97 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 98 | 99 | jade@0.26.3: 100 | version "0.26.3" 101 | resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" 102 | dependencies: 103 | commander "0.6.1" 104 | mkdirp "0.3.0" 105 | 106 | jszip@^3.1.3: 107 | version "3.1.5" 108 | resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.5.tgz#e3c2a6c6d706ac6e603314036d43cd40beefdf37" 109 | dependencies: 110 | core-js "~2.3.0" 111 | es6-promise "~3.0.2" 112 | lie "~3.1.0" 113 | pako "~1.0.2" 114 | readable-stream "~2.0.6" 115 | 116 | lie@~3.1.0: 117 | version "3.1.1" 118 | resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" 119 | dependencies: 120 | immediate "~3.0.5" 121 | 122 | lru-cache@2: 123 | version "2.7.3" 124 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" 125 | 126 | minimatch@0.3: 127 | version "0.3.0" 128 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" 129 | dependencies: 130 | lru-cache "2" 131 | sigmund "~1.0.0" 132 | 133 | minimatch@^3.0.4: 134 | version "3.0.4" 135 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 136 | dependencies: 137 | brace-expansion "^1.1.7" 138 | 139 | minimist@0.0.8: 140 | version "0.0.8" 141 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 142 | 143 | mkdirp@0.3.0: 144 | version "0.3.0" 145 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" 146 | 147 | mkdirp@0.5.1: 148 | version "0.5.1" 149 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 150 | dependencies: 151 | minimist "0.0.8" 152 | 153 | mocha@^2.2.5: 154 | version "2.5.3" 155 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" 156 | dependencies: 157 | commander "2.3.0" 158 | debug "2.2.0" 159 | diff "1.4.0" 160 | escape-string-regexp "1.0.2" 161 | glob "3.2.11" 162 | growl "1.9.2" 163 | jade "0.26.3" 164 | mkdirp "0.5.1" 165 | supports-color "1.2.0" 166 | to-iso-string "0.0.2" 167 | 168 | ms@0.7.1: 169 | version "0.7.1" 170 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 171 | 172 | once@^1.3.0: 173 | version "1.4.0" 174 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 175 | dependencies: 176 | wrappy "1" 177 | 178 | os-tmpdir@~1.0.1: 179 | version "1.0.2" 180 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 181 | 182 | pako@~1.0.2: 183 | version "1.0.6" 184 | resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" 185 | 186 | path-is-absolute@^1.0.0: 187 | version "1.0.1" 188 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 189 | 190 | prettier@^1.10.2: 191 | version "1.10.2" 192 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" 193 | 194 | process-nextick-args@~1.0.6: 195 | version "1.0.7" 196 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 197 | 198 | readable-stream@~2.0.6: 199 | version "2.0.6" 200 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" 201 | dependencies: 202 | core-util-is "~1.0.0" 203 | inherits "~2.0.1" 204 | isarray "~1.0.0" 205 | process-nextick-args "~1.0.6" 206 | string_decoder "~0.10.x" 207 | util-deprecate "~1.0.1" 208 | 209 | rimraf@^2.5.4: 210 | version "2.6.2" 211 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" 212 | dependencies: 213 | glob "^7.0.5" 214 | 215 | sax@>=0.6.0: 216 | version "1.2.4" 217 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" 218 | 219 | selenium-webdriver@^3.5.0: 220 | version "3.6.0" 221 | resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" 222 | dependencies: 223 | jszip "^3.1.3" 224 | rimraf "^2.5.4" 225 | tmp "0.0.30" 226 | xml2js "^0.4.17" 227 | 228 | sigmund@~1.0.0: 229 | version "1.0.1" 230 | resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" 231 | 232 | string_decoder@~0.10.x: 233 | version "0.10.31" 234 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 235 | 236 | supports-color@1.2.0: 237 | version "1.2.0" 238 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" 239 | 240 | tmp@0.0.30: 241 | version "0.0.30" 242 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" 243 | dependencies: 244 | os-tmpdir "~1.0.1" 245 | 246 | to-iso-string@0.0.2: 247 | version "0.0.2" 248 | resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" 249 | 250 | util-deprecate@~1.0.1: 251 | version "1.0.2" 252 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 253 | 254 | wrappy@1: 255 | version "1.0.2" 256 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 257 | 258 | xml2js@^0.4.17: 259 | version "0.4.19" 260 | resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" 261 | dependencies: 262 | sax ">=0.6.0" 263 | xmlbuilder "~9.0.1" 264 | 265 | xmlbuilder@~9.0.1: 266 | version "9.0.4" 267 | resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" 268 | --------------------------------------------------------------------------------