├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── app ├── console.js ├── frame.css ├── frame.html ├── grep.js ├── index.html ├── index.js ├── layout.js ├── options.js ├── run.js ├── socket.js ├── style.css └── templates.js ├── bin └── prova ├── docs ├── examples └── man ├── example.js ├── index.js ├── lib ├── browser-command.js ├── browser-reporter.js ├── browser.js ├── browserify-transforms.js ├── cli.js ├── command.js ├── exec-first.js ├── launch.js ├── node-command.js ├── node-reporter.js ├── node-template.js ├── progress.js ├── refine.js └── tests.js ├── package.json ├── templates ├── code.html ├── custom-frame.html ├── diff.html ├── error.html ├── frame.html ├── layout.html ├── node-browser-console.txt ├── node-browser-error-browser.txt ├── node-browser-error-no-stack.txt ├── node-browser-error.txt ├── node-browser-failed-result.txt ├── node-browser-instructions.txt ├── node-browser-launch.txt ├── node-browser-passed-result.txt ├── node-browser-test.txt ├── node-browser.txt ├── node-diff.txt ├── node-fail.txt ├── node-result-pass.txt ├── node-result.txt ├── overview.html ├── pass.html ├── stack-line.html ├── stack.html ├── test.html └── waiting.html └── test ├── altjs ├── test.coffee └── test.gs ├── custom-frame.html ├── error.js ├── index.js ├── multiple.js ├── slow-errors.js ├── slow.js └── test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | app/templates.js 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | test.js 3 | example 4 | examples 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | copyright (c) 2015, Azer Koçulu 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## prova 2 | 3 | Node & Browser Test runner based on [Tape](http://github.com/substack/tape) and [Browserify](http://github.com/substack/node-browserify). 4 | 5 | Screencasts: [node.gif](https://dl.dropboxusercontent.com/s/8yyepixc0bbtby3/prova-node.gif), [browser.gif](https://dl.dropboxusercontent.com/s/wtzt78riv7vcp7n/prova.gif), [both.gif](https://i.cloudup.com/4jGix1WEDH.gif), [headless browser](https://i.cloudup.com/lWWplVaKta.png) 6 | 7 | Slides: [slides.com/azer/prova](http://slides.com/azer/prova) 8 | 9 | Features and screenshots: 10 | 11 | * Embeds [Tape](http://github.com/substack/tape) 12 | * Comes with a builtin web app to run tests on browser, sourcemaps are enabled. 13 | * Outputs less when tests pass ([Node](https://i.cloudup.com/ausJApnH1v.png), [Browser](https://i.cloudup.com/OKebjyRMfU.png)) 14 | * Outputs more when tests fail ([Node](https://i.cloudup.com/R8KQ8Qwspz.png), [Browser](https://i.cloudup.com/nA08e0s60b.png)) 15 | * Browser app runs tests inside of an iframe [Screenshot](https://i.cloudup.com/5n8H9AqMrf.png) 16 | * Uses [watchify](https://github.com/substack/watchify) to observe file changes and restart browser tests. [GIF Screenshot](https://dl.dropboxusercontent.com/s/wtzt78riv7vcp7n/prova.gif) 17 | * Lets filtering test cases (e.g node test.js -g foobar) 18 | * Comes with [browser-launcher](https://github.com/substack/browser-launcher) for [launching browsers automatically and headless testing](#launching-browsers-and-headless-testing). ([Screenshot](https://i.cloudup.com/lWWplVaKta.png)) 19 | * Clickable error stacks on the browser: [Screenshot](https://i.cloudup.com/42iYw0WnPP.gif) 20 | * Optional progress bar for slow tests. GIFs: [Node](https://i.cloudup.com/PJR44iZStH.gif) / [Browser](https://cldup.com/n5miJIEN2G.gif) 21 | 22 | ## Install 23 | 24 | ```bash 25 | $ npm install -g azer/prova 26 | ``` 27 | 28 | ## Usage 29 | 30 | Example test: 31 | 32 | ```js 33 | var test = require('prova') 34 | 35 | test('timing test', function (t) { 36 | t.plan(2) 37 | 38 | t.equal(typeof Date.now, 'function') 39 | var start = Date.now() 40 | 41 | setTimeout(function () { 42 | t.equal(Date.now() - start, 100) 43 | }, 100) 44 | }) 45 | ``` 46 | 47 | In Node, it will output: 48 | 49 | ``` 50 | $ node test.js 51 | Passed 1 test. 52 | ``` 53 | 54 | Or, in case it fails: 55 | 56 | ![](https://i.cloudup.com/R8KQ8Qwspz.png) 57 | 58 | ### In Browser 59 | 60 | To run the tests in a web browser, just pass `-b` parameter: 61 | 62 | ```bash 63 | $ node test.js -b 64 | Visit localhost:7559 with a browser to start running the tests. 65 | ``` 66 | 67 | Then visit `localhost:7559` in your web browser: 68 | 69 | ![](https://i.cloudup.com/OKebjyRMfU.png) 70 | 71 | In case it fails, it'll show: 72 | 73 | ![](https://i.cloudup.com/nA08e0s60b.png) 74 | 75 | The web app uses [watchify](http://github.com/substack/watchify) to monitor file changes. 76 | So, you won't have to reload the page when you modify a source code. 77 | 78 | Prova runs the tests inside of an iframe. In case you test some UI stuff, you can open the iframe 79 | by clicking the `<` button on the right: 80 | 81 | ![](https://i.cloudup.com/5n8H9AqMrf.png) 82 | 83 | ### Multiple Tests 84 | 85 | Prova comes with a command-line script when you install it globally; 86 | 87 | ```bash 88 | $ npm install -g prova 89 | ``` 90 | 91 | And it allows you running multiple tests on both Node and browser; 92 | 93 | ```bash 94 | $ prova test/foo.js test/bar.js 95 | ``` 96 | 97 | ```bash 98 | $ prova test/**/*.js -b 99 | ``` 100 | 101 | ### Launching Browsers and Headless Testing 102 | 103 | List the detected browsers; 104 | 105 | ```bash 106 | $ prova -l 107 | Available Browsers: safari v7.0.2, chrome v34.0.1847.116, phantom v1.9.7 108 | ``` 109 | 110 | And launch after publishing the tests: 111 | 112 | ```bash 113 | $ prova -b -l safari 114 | ``` 115 | 116 | If your system has Xvfb, you can pass `-e` parameter to open the browser headlessly: 117 | 118 | ```bash 119 | $ prova -b -l chrome -e 120 | ``` 121 | 122 | Or you can just run the tests on PhantomJS: 123 | 124 | ```bash 125 | $ prova -b -l phantom 126 | ``` 127 | 128 | If you get `no matches for` errors and you think that your system has that browser, try removing [browser-launcher](https://github.com/substack/browser-launcher)'s config: 129 | 130 | ```bash 131 | $ rm /Users/azer/.config/browser-launcher/config.json 132 | ``` 133 | 134 | ### Browserify Transforms 135 | 136 | Prova automatically applies [bunch of transforms](https://github.com/azer/prova/blob/master/lib/browserify-transforms.js#L4) by looking at the file extension. If you'd like to use a transform that doesn't exist in Prova by default, you can choose it with a parameter; 137 | 138 | ```bash 139 | $ node test -b -t coffeeify 140 | ``` 141 | 142 | Multiple transforms can be specified using comma; 143 | 144 | ```bash 145 | $ node test -b -t coffeeify,brfs,foo,bar 146 | ``` 147 | 148 | ### Browserify Plugins 149 | 150 | Pass Browserify plugins passing `-u` or `--plugin` parameter; 151 | 152 | ```bash 153 | $ node test -b --plugin foo 154 | ``` 155 | 156 | Use comma to separate multiple plugins; 157 | 158 | ```bash 159 | $ node test -b --plugin foo,bar 160 | ``` 161 | 162 | ### Custom Frame Documents 163 | 164 | When you're running the tests on the browser, Prova has an empty HTML template that loads and runs the JavaScript tests. 165 | You can customize this HTML file with -f or --frame parameter: 166 | 167 | ```js 168 | $ node test -b -f test.html 169 | ``` 170 | 171 | Click the arrow button on right middle to keep the frame open. You'll be seeing the HTML document and test results in the same screen. 172 | 173 | ### Manually Restarting Browser Tests 174 | 175 | Prova watches for changes and automatically restarts the browser tests (inside in an iframe) but in case you need, there is an endpoint for restarting all the tests by hitting an endpoint; 176 | 177 | ``` 178 | $ curl localhost:7559/restart 179 | ``` 180 | 181 | ### Loading Assets 182 | 183 | You may need to load your images, web workers etc. for testing. Prova allows you to load assets from your current directory via the `/assets/in` endpoint. Let's say you'd like to load a file called "foobar.png": 184 | 185 | ``` 186 | $ curl http://localhost:7559/assets/in/foobar.png 187 | ``` 188 | 189 | Should work for you. 190 | 191 | ### HTTP Proxy 192 | 193 | HTTP proxying is pretty useful to by-pass cross-domain issues (CORS) on the browser. You can easily point a URL to another host using `-y` / `--http-proxy` parameters: 194 | 195 | ``` 196 | $ node test -b -y "/my-api=http://localhost:8080" 197 | ``` 198 | 199 | Assuming that you'll be running your tests on `:7559`, any requests to `/my-api` will be streamed through `localhost:8080` in the above example. 200 | 201 | ## Command-line 202 | 203 | ``` 204 | USAGE 205 | 206 | prova [filenames] [options] 207 | 208 | OPTIONS 209 | 210 | -g --grep Run tests matching with given pattern 211 | 212 | -b --browser Publishes the tests on 0.0.0.0:7559 213 | -o --port Publish the tests on given port number. 214 | -d --hostname Publish the tests on given hostname. 215 | -l --launch List available browsers to launch or launch specified browser. 216 | -e --headless Launch the browser headlessly. (Requires xvfb) 217 | -r --proxy Launch the browser with specified proxy configuration. 218 | -q --quit Shut down the browser server once all the tests are done. 219 | -f --frame Specify a custom document to run tests on browser. e.g node test -b -f custom.html 220 | -x --exec Execute given commmand before running the tests. 221 | -y --http-proxy Proxy requests matching with given pattern to a target. e.g -y "/images=localhost:8080" 222 | 223 | -t --transform Use given Browserify transforms. e.g node test -b -t coffeeify,brfs 224 | -u --plugin Use given Browserify plugins. e.g node test -b -u foo,bar 225 | 226 | -s --progress Show a progress bar. Useful when tests are running slow. 227 | 228 | -p --tap Output original Tap output without modifying anything. 229 | 230 | -C --no-console Disable showing browser console messages on command-line. 231 | 232 | -v --version Show version and exit 233 | -h --help Show help and exit 234 | --examples Show example commands 235 | ``` 236 | 237 | ## Example Commands 238 | 239 | ``` 240 | EXAMPLES 241 | 242 | 1. Run the tests on NodeJS. 243 | 244 | $ node test.js 245 | $ node test 246 | $ prova test/index.js 247 | $ prova 248 | 249 | All the above example commands will work same way. Prova assumes the filename of your test is either `test.js` or `test/index.js` 250 | 251 | 2. Publish the tests on localhost:7559, so you can run the tests on a web browser. 252 | 253 | $ node test.js -b 254 | $ prova test -b 255 | $ prova -b 256 | 257 | 3. Publish the tests on given host and port. 258 | 259 | $ node test.js -o 8080 -d foobar.net 260 | $ prova test.js -o 8080 -d foobar.net 261 | 262 | 4. Publish the tests and launch a browser to automatically run the tests. 263 | 264 | $ node test.js -b -l chrome 265 | $ prova test.js -b -l chrome 266 | 267 | 5. List the browsers that can be launched automatically. 268 | 269 | $ prova -l 270 | $ node test.js -l 271 | 272 | 6. Run the tests with PhantomJS. 273 | 274 | $ node test.js -b -l phantom 275 | $ prova test.js -b -l phantom 276 | 277 | 7. Run only specified tests with PhantomJS. 278 | 279 | $ node test.js -b -l phantom -g pattern 280 | $ prova test.js -b -l phantom -g pattern 281 | 282 | 8. Launch Chrome headlessly using xvfb: 283 | 284 | $ node test -b -l chrome -e 285 | $ prova test -b -l chrome -e 286 | ``` 287 | -------------------------------------------------------------------------------- /app/console.js: -------------------------------------------------------------------------------- 1 | var socket = require("./socket"); 2 | 3 | module.exports = { 4 | override: all 5 | }; 6 | 7 | function all () { 8 | var rpl = {}; 9 | var name; 10 | for (name in console) { 11 | rpl[name] = override(name, console[name], console); 12 | } 13 | 14 | window.console = rpl; 15 | } 16 | 17 | function override (name, fn, console) { 18 | return function () { 19 | fn.apply(console, arguments); 20 | 21 | if (name == 'clear' || name == 'memory') { 22 | return; 23 | } 24 | 25 | var params = Array.prototype.slice.call(arguments); 26 | 27 | try { 28 | JSON.stringify(params); 29 | } catch (exc) { 30 | return; 31 | } 32 | 33 | socket.send({ 34 | console: true, 35 | method: name, 36 | params: params 37 | }); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /app/frame.css: -------------------------------------------------------------------------------- 1 | html, body, .prova { 2 | width: 100%; 3 | height: 100%; 4 | padding: 0; 5 | margin: 0; 6 | background: #fff; 7 | } 8 | 9 | .prova { 10 | display:-ms-flexbox; 11 | -ms-flex-pack:center; 12 | -ms-flex-align:center; 13 | 14 | display:-moz-box; 15 | -moz-box-pack:center; 16 | -moz-box-align:center; 17 | 18 | display:-webkit-box; 19 | -webkit-box-pack:center; 20 | -webkit-box-align:center; 21 | 22 | display:box; 23 | box-pack:center; 24 | box-align:center; 25 | 26 | font: normal 12px "pt mono"; 27 | color: #333; 28 | } 29 | 30 | @font-face { 31 | font-family: 'PT Mono'; 32 | font-style: normal; 33 | font-weight: 400; 34 | src: local('PT Mono'), local('PTMono-Regular'), url(http://themes.googleusercontent.com/static/fonts/ptmono/v2/RTA0Dhj_HESY0h-ax44VkwLUuEpTyoUstqEm5AMlJo4.woff) format('woff'); 35 | } 36 | -------------------------------------------------------------------------------- /app/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Prova Frame 6 | 7 | 8 | 9 |
Nothing to show here.
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/grep.js: -------------------------------------------------------------------------------- 1 | var options = require("./options"); 2 | 3 | module.exports = grep; 4 | 5 | function grep (value) { 6 | if (arguments.length > 0) { 7 | set(value); 8 | } 9 | 10 | return get(); 11 | } 12 | 13 | function set (value) { 14 | var all = options.read(); 15 | all.grep = value; 16 | options.write(all); 17 | } 18 | 19 | function get () { 20 | return options.read()['grep']; 21 | } 22 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Prova 6 | 7 | 8 | 9 | {layout} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | var layout = require("./layout"); 2 | var run = require("./run"); 3 | var socket = require("./socket"); 4 | var console = require("./console"); 5 | 6 | console.override(); 7 | 8 | socket(function (update) { 9 | if (!update || !update.message) return; 10 | if (update.message.start) { 11 | run(update.message.url); 12 | } 13 | 14 | if (update.message.restart) run(); 15 | }); 16 | -------------------------------------------------------------------------------- /app/layout.js: -------------------------------------------------------------------------------- 1 | var dom = require("dom-tree"); 2 | var on = require("dom-event"); 3 | on.child = require("component-delegate"); 4 | 5 | var select = require("dom-select"); 6 | var style = require("dom-style"); 7 | var classes = require("dom-classes"); 8 | var format = require("format-text"); 9 | var bindKey = require("key-event"); 10 | var escape = require("escape-html"); 11 | var failingCode = require("failing-code"); 12 | var socket = require("./socket"); 13 | var grep = require("./grep"); 14 | var templates = require("./templates"); 15 | var options = require("./options"); 16 | var url; 17 | 18 | on(window, 'resize', updatePositions); 19 | on(window, 'hashchange', run); 20 | on(window, 'scroll', saveScrollState); 21 | socket(updateConn); 22 | 23 | module.exports = { 24 | addError: addError, 25 | markTest: markTest, 26 | pass: pass, 27 | status: status, 28 | run: run, 29 | list: list 30 | }; 31 | 32 | function addError (error) { 33 | var top = select('.top'); 34 | var view = { 35 | name: error.name 36 | }; 37 | 38 | if (!top) { 39 | setup(); 40 | top = select('.top'); 41 | }; 42 | 43 | classes.add(select('.top'), 'failed'); 44 | view.stack = format(templates.stack, escape(error.stack).replace(/\n\s+/g, templates['stack-line'])); 45 | 46 | view.code = error.source.length ? formatCode(error.source) : ''; 47 | 48 | if (error.expected != undefined) { 49 | view.diff = format(templates.diff, escape(JSON.stringify(error.expected, null, " ")), escape(JSON.stringify(error.actual, null, " "))) || " "; 50 | view['diff-class'] = ''; 51 | } else { 52 | view.diff = ""; 53 | view['diff-class'] = ' empty'; 54 | } 55 | 56 | if (addError.last != error.test) { 57 | view.title = '

' + error.test +'

'; 58 | addError.last = error.test; 59 | } else { 60 | view.title = ''; 61 | } 62 | 63 | if (!addError.counter) { 64 | addError.counter = 1; 65 | } 66 | 67 | view.id = addError.counter++; 68 | 69 | dom.add(select('.results .errors'), templates.error, view); 70 | var el = select('#error-' + view.id); 71 | on.child.bind(el, '.stack-line', 'click', function (e) { 72 | var index = Array.prototype.indexOf.call(e.target.parentElement.children, e.target) - 1; 73 | select('.code', el).innerHTML = formatCode(failingCode(error, __source_code, index)); 74 | }); 75 | } 76 | 77 | function markTest (test) { 78 | var overview = select('.overview'); 79 | if (overview) return; 80 | 81 | var list = select('.waiting .list'); 82 | 83 | if (!list) { 84 | dom.add(select('.waiting h1'), templates.overview); 85 | list = select('.waiting .list'); 86 | } 87 | 88 | dom.add(list, templates.test, { 89 | icon: '', 90 | name: test.name 91 | }); 92 | } 93 | 94 | function setup () { 95 | dom.remove(select('.waiting')); 96 | 97 | var template = format(templates['layout'], templates); 98 | dom.add(document.body, template, { 99 | grep: grep() || '', 100 | conn: '' 101 | }); 102 | 103 | on(select('.frame-button'), 'click', toggleFrame); 104 | on(select('.run-again'), 'click', run); 105 | on(select('.maximize'), 'click', maximize); 106 | on(select('.minimize'), 'click', minimize); 107 | 108 | updateConn(); 109 | 110 | setupGrep(); 111 | 112 | if (localStorage['frame-open']) { 113 | toggleFrame(); 114 | } 115 | 116 | if (localStorage['frame-maximized']) { 117 | maximize(); 118 | } 119 | 120 | updateFramePosition(); 121 | recoverScrollPosition(); 122 | } 123 | 124 | function setupGrep () { 125 | var el = select('#grep'); 126 | 127 | on(select('.grep label'), 'click', function () { 128 | el.focus(); 129 | }); 130 | 131 | bindKey(el, 'enter', function () { 132 | grep(el.value); 133 | }); 134 | } 135 | 136 | function run (_url) { 137 | if (typeof _url == 'string') { 138 | url = _url; 139 | } 140 | 141 | addError.last = undefined; 142 | status('running'); 143 | 144 | dom.add(document.body, templates['frame'], { 145 | url: url, 146 | options: options.stringify() 147 | }); 148 | } 149 | 150 | function end () { 151 | classes.remove(select('.waiting'), '.running'); 152 | } 153 | 154 | function pass (assertions) { 155 | setup(); 156 | classes.add(select('.top'), 'passed'); 157 | select('.pass').innerHTML = format(templates.pass, assertions); 158 | classes.add(select('.results'), 'passed'); 159 | } 160 | 161 | function toggleFrame () { 162 | var isOpen = classes.has(select('.results'), 'open'); 163 | 164 | if (isOpen) { 165 | closeFrame(); 166 | } else { 167 | openFrame(); 168 | } 169 | 170 | updateFramePosition(); 171 | } 172 | 173 | function openFrame () { 174 | var results = select('.results'); 175 | var frame = select('.frame'); 176 | 177 | classes.add(results, 'open'); 178 | classes.add(frame, 'open'); 179 | 180 | localStorage['frame-open'] = true; 181 | } 182 | 183 | function closeFrame () { 184 | var results = select('.results'); 185 | var frame = select('.frame'); 186 | 187 | classes.remove(results, 'open'); 188 | classes.remove(frame, 'open'); 189 | 190 | delete localStorage['frame-open']; 191 | } 192 | 193 | function updatePositions () { 194 | updateFramePosition(); 195 | } 196 | 197 | function updateFramePosition () { 198 | var frame = select('.frame'); 199 | var results = select('.results'); 200 | 201 | if (!frame) return; 202 | 203 | var isOpen = classes.has(frame, 'open') && !classes.has(select('body'), 'maximized'); 204 | var right = isOpen ? results.offsetWidth : 0; 205 | 206 | style(select('.frame-button'), 'right', right + 'px'); 207 | 208 | if (!isOpen) return; 209 | 210 | style(frame, { 211 | width: results.offsetWidth - 1 + 'px', 212 | left: right + 'px', 213 | height: '100%' 214 | }); 215 | } 216 | 217 | function status (msg) { 218 | var el = select('.status'); 219 | 220 | if (!el) { 221 | document.body.innerHTML = format(templates.waiting, { 222 | message: msg 223 | }); 224 | return; 225 | } 226 | 227 | el.innerHTML = msg; 228 | select('.waiting').className = 'waiting center ' + msg; 229 | } 230 | 231 | function list (tests) { 232 | var key; 233 | for (key in tests) { 234 | dom.add(select('.overview .list'), templates.test, { 235 | icon: tests[key] ? '✓' : '✖', 236 | name: key 237 | }); 238 | } 239 | } 240 | 241 | function updateConn (msg) { 242 | var el = select('.conn'); 243 | if (!el) return; 244 | 245 | el.innerHTML = socket.isOpen() ? 'Watching File Changes' : 'Disconnected'; 246 | } 247 | 248 | function maximize () { 249 | classes.add(select('body'), 'maximized'); 250 | localStorage['frame-maximized'] = true; 251 | updateFramePosition(); 252 | } 253 | 254 | function minimize () { 255 | delete localStorage['frame-maximized']; 256 | classes.remove(select('body'), 'maximized'); 257 | updateFramePosition(); 258 | } 259 | 260 | function saveScrollState () { 261 | if (saveScrollState.defer) { 262 | clearTimeout(saveScrollState.defer); 263 | saveScrollState.defer = undefined; 264 | } 265 | 266 | saveScrollState.defer = setTimeout(function () { 267 | localStorage['scrollTop'] = document.body.scrollTop; 268 | }, 500); 269 | } 270 | 271 | function recoverScrollPosition () { 272 | document.body.scrollTop = Number(localStorage['scrollTop']); 273 | } 274 | 275 | function formatCode (source) { 276 | return format(templates.code, { 277 | 'first-line-num': source[0].line, 278 | 'first-line-source': escape(source[0].code), 279 | 'second-line-num': source[1].line, 280 | 'second-line-source': escape(source[1].code), 281 | 'third-line-num': source[2].line, 282 | 'third-line-source': escape(source[2].code) 283 | }) 284 | } 285 | -------------------------------------------------------------------------------- /app/options.js: -------------------------------------------------------------------------------- 1 | var querystring = require("querystring"); 2 | 3 | module.exports = { 4 | read: read, 5 | write: write, 6 | stringify: stringify 7 | }; 8 | 9 | function stringify () { 10 | return document.location.hash.slice(1); 11 | } 12 | 13 | function read () { 14 | var hash = stringify(); 15 | if (!hash) return {}; 16 | return querystring.parse(hash, ';', ':'); 17 | } 18 | 19 | function write (values) { 20 | var str = querystring.stringify(values, ';', ':'); 21 | var url = document.location.href.split('#')[0]; 22 | document.location.href = url + '#' + str; 23 | } 24 | -------------------------------------------------------------------------------- /app/run.js: -------------------------------------------------------------------------------- 1 | var select = require("dom-select"); 2 | var layout = require("./layout"); 3 | var socket = require("./socket"); 4 | 5 | module.exports = start; 6 | 7 | function start (url) { 8 | window.addEventListener("message", receiveMessage, false); 9 | 10 | // clear the console on each run so that previous messages don't show 11 | if (window.console && console.clear) { 12 | console.clear(); 13 | } 14 | 15 | layout.run(url); 16 | } 17 | 18 | function receiveMessage (message) { 19 | if (message.data.type == 'error') { 20 | fail(message.data); 21 | } 22 | 23 | if (message.data.type == 'test') { 24 | test(message.data); 25 | } 26 | 27 | if (message.data.type == 'end') { 28 | end(message.data); 29 | } 30 | } 31 | 32 | function test (msg) { 33 | layout.markTest(msg); 34 | socket.send({ 35 | test: msg.name, 36 | userAgent: navigator.userAgent 37 | }); 38 | } 39 | 40 | function fail (error) { 41 | socket.send({ fail: error, userAgent: navigator.userAgent }); 42 | layout.addError(error); 43 | } 44 | 45 | function end (result) { 46 | socket.send({ userAgent: navigator.userAgent, result: result }); 47 | 48 | if (!result.failed) layout.pass(result.passed); 49 | layout.list(result.tests); 50 | } 51 | -------------------------------------------------------------------------------- /app/socket.js: -------------------------------------------------------------------------------- 1 | var pubsub = require("pubsub")(); 2 | var status = false; 3 | var ws; 4 | 5 | connect(); 6 | 7 | module.exports = pubsub; 8 | module.exports.isOpen = isOpen; 9 | module.exports.send = send; 10 | 11 | function connect () { 12 | ws = window.ws = new WebSocket(document.location.origin.replace('http', 'ws')); 13 | ws.onopen = open; 14 | ws.onmessage = message; 15 | ws.onclose = close; 16 | } 17 | 18 | function isOpen () { 19 | return status; 20 | } 21 | 22 | function open () { 23 | status = true; 24 | pubsub.publish({ open: true }); 25 | } 26 | 27 | function message (event) { 28 | pubsub.publish({ 29 | message: JSON.parse(event.data) 30 | }); 31 | } 32 | 33 | function close () { 34 | if (status == false) return; 35 | status = false; 36 | pubsub.publish({ close: true }); 37 | reconnect(); 38 | } 39 | 40 | function reconnect () { 41 | if (status) return; 42 | connect(); 43 | setTimeout(reconnect, 1000); 44 | } 45 | 46 | function send (message) { 47 | ws.send(JSON.stringify(message)); 48 | }; 49 | -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Open Sans'), local('OpenSans'), url(http://themes.googleusercontent.com/static/fonts/opensans/v8/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'PT Mono'; 10 | font-style: normal; 11 | font-weight: 400; 12 | src: local('PT Mono'), local('PTMono-Regular'), url(http://themes.googleusercontent.com/static/fonts/ptmono/v2/RTA0Dhj_HESY0h-ax44VkwLUuEpTyoUstqEm5AMlJo4.woff) format('woff'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Vollkorn'; 17 | font-style: normal; 18 | font-weight: 400; 19 | src: local('Vollkorn Regular'), local('Vollkorn-Regular'), url(http://themes.googleusercontent.com/static/fonts/vollkorn/v4/BCFBp4rt5gxxFrX6F12DKnYhjbSpvc47ee6xR_80Hnw.woff) format('woff'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Abril Fatface'; 24 | font-style: normal; 25 | font-weight: 400; 26 | src: local('Abril Fatface'), local('AbrilFatface-Regular'), url(http://themes.googleusercontent.com/static/fonts/abrilfatface/v6/X1g_KwGeBV3ajZIXQ9VnDokaQb-UsZVONjobs91YQtw.woff) format('woff'); 27 | } 28 | 29 | html, body { 30 | width:100%; height:100%; 31 | margin:0; padding:0; 32 | -webkit-font-smoothing: subpixel-antialiased; 33 | text-rendering: optimizeLegibility; 34 | } 35 | 36 | .waiting { 37 | font: 800 36px/88px "open sans"; 38 | text-align: center; 39 | color: #fff; 40 | width: 100%; 41 | height: 100%; 42 | background: rgb(156, 229, 230); 43 | color: #fff; 44 | text-shadow:2px 2px rgba(0, 0, 0, 0.2); 45 | } 46 | 47 | .waiting h1 { 48 | display: block; 49 | text-align: center; 50 | width: 100%; 51 | } 52 | 53 | .waiting .list { 54 | font: normal 12px "pt mono"; 55 | color: #8b98ac; 56 | text-shadow: none; 57 | list-style: none; 58 | padding: 10px 0; 59 | } 60 | 61 | .waiting .status { 62 | margin: 0; 63 | } 64 | 65 | .results { 66 | width: 100%; 67 | } 68 | 69 | .results.passed { 70 | height: 100%; 71 | } 72 | 73 | .top { 74 | min-height: 100%; 75 | background: rgb(252, 250, 242); 76 | padding-bottom: 50px; 77 | } 78 | 79 | .top.failed { 80 | background: rgb(239, 71, 35); 81 | } 82 | 83 | .top.passed { 84 | background: #219d4c; 85 | height: 100%; 86 | } 87 | 88 | .top .pass { 89 | color: #fff; 90 | height: 100%; 91 | font: 800 21px/40px "open sans"; 92 | text-align: center; 93 | text-shadow:2px 2px rgba(0, 0, 0, 0.2); 94 | height: 100%; 95 | } 96 | 97 | .errors { 98 | list-style: none; 99 | padding:10px 30px; 100 | } 101 | 102 | .errors li { 103 | background: rgb(209, 71, 35); 104 | margin: 10px 0; 105 | } 106 | 107 | .errors li .code { 108 | padding: 5px; 109 | background: #333; 110 | margin: 0; 111 | } 112 | 113 | .errors h3 { 114 | color: #fff; 115 | font: 400 24px "vollkorn"; 116 | font-weight: normal; 117 | letter-spacing: 1px; 118 | background: rgb(239, 71, 35); 119 | padding: 10px 0; 120 | } 121 | 122 | .errors h4 { 123 | font: 16px/16px "open sans"; 124 | 125 | background: rgb(170, 66, 34); 126 | color: rgb(255, 225, 0); 127 | padding: 10px 10px; 128 | margin: 0; 129 | } 130 | 131 | .errors .diff { 132 | font: 12px/12px "open sans"; 133 | 134 | background: rgb(45, 45, 45); 135 | color: rgb(255, 255, 255); 136 | padding: 10px 10px; 137 | margin: 0; 138 | } 139 | 140 | .errors .diff .actual { 141 | padding-top: 5px; 142 | } 143 | 144 | .errors .diff.empty { 145 | display: none; 146 | } 147 | 148 | .errors .diff strong { 149 | padding-right: 5px; 150 | color: #ccc; 151 | } 152 | 153 | .errors .stack { 154 | padding: 10px 10px; 155 | 156 | padding: 5px 20px; 157 | font: 13px/21px "open sans"; 158 | overflow: hidden; 159 | } 160 | 161 | .errors .code .line { 162 | overflow: hidden; 163 | white-space: pre; 164 | margin: 0; 165 | font: 13px/21px "open sans"; 166 | color: #888; 167 | padding: 2px; 168 | font-weight: bold; 169 | } 170 | 171 | .errors .code .failing-line { 172 | color: #fff; 173 | } 174 | 175 | .errors .code .line span { 176 | padding-right: 3px; 177 | } 178 | 179 | .errors .stack { 180 | font: normal 12px "pt mono"; 181 | } 182 | 183 | .errors .stack-title { 184 | color: rgb(64, 44, 44); 185 | padding: 10px 0 5px; 186 | } 187 | 188 | .errors .stack-line { 189 | padding: 1px 20px; 190 | color: rgb(64, 44, 44); 191 | } 192 | 193 | .errors .stack-line:hover { 194 | cursor: pointer; 195 | color: #fff; 196 | } 197 | 198 | .results ul, .results h3 { 199 | margin: 0; 200 | } 201 | 202 | .frame-button { 203 | display: block; 204 | position: absolute; 205 | right: 0; 206 | width: 25px; 207 | height: 100%; 208 | float: right; 209 | color: rgba(0, 0, 0, 0.5); 210 | font: bold 24px Arial, sans-serif; 211 | padding-right: 5px; 212 | } 213 | 214 | .frame-button:hover { 215 | color: rgba(0, 0, 0, 0.7); 216 | } 217 | 218 | .frame-button .close { 219 | display: none; 220 | } 221 | 222 | .results.open .frame-button .open { 223 | display: none; 224 | } 225 | 226 | .results.open .frame-button .close { 227 | display: inline; 228 | } 229 | 230 | .results.open { 231 | float: left; 232 | width: 50%; 233 | } 234 | 235 | .maximized .frame { 236 | position: static !important; 237 | width: 100% !important; 238 | height: 100% !important; 239 | display: block !important; 240 | } 241 | 242 | .maximized .frame-button { 243 | display: none; 244 | } 245 | 246 | .maximized .results { 247 | width: 100%; 248 | float: none; 249 | } 250 | 251 | a.minimize, .maximized a.maximize { 252 | display: none; 253 | } 254 | 255 | .maximized a.minimize { 256 | display: inline; 257 | } 258 | 259 | .running { 260 | background: #feffeb; 261 | color: #b38db4; 262 | } 263 | 264 | .frame { 265 | display: none; 266 | background: rgba(255, 255, 255, 0.2); 267 | border: 0; 268 | } 269 | 270 | .frame.open { 271 | display: block; 272 | position: absolute; 273 | border: 0; 274 | padding: 0; 275 | margin: 0; 276 | } 277 | 278 | .navigation { 279 | width: 100%; 280 | height: 40px; 281 | background: rgba(0, 0, 0, 0.1); 282 | position: absolute; 283 | margin-top: -50px; 284 | text-align: center; 285 | padding: 0 0 10px; 286 | } 287 | 288 | .navigation .buttons { 289 | width: 500px; 290 | margin: 0 auto; 291 | background: rgba(0, 0, 0, 0.1); 292 | padding: 20px 0 18px 0; 293 | font: 12px Arial, sans-serif; 294 | } 295 | 296 | .navigation .buttons a { 297 | g font: normal 12px "pt mono"; 298 | color: rgba(255, 255, 255, 0.8); 299 | text-transform: uppercase; 300 | margin-right: 20px; 301 | } 302 | 303 | .navigation .buttons a:hover { 304 | color: rgba(255, 255, 255, 1); 305 | border-bottom: 1px dotted rgba(255, 255, 255, 0.5); 306 | } 307 | 308 | .overview { 309 | background: rgb(42, 44, 57); 310 | } 311 | 312 | .overview ul { 313 | width: 500px; 314 | padding: 50px 30px 0 30px; 315 | color: #fff; 316 | list-style: none; 317 | margin: 0 auto; 318 | font: 14px Helvetica, Arial, sans-serif; 319 | } 320 | 321 | .overview li { 322 | padding: 10px 10px 10px; 323 | background: rgba(30, 30, 30, 0.3); 324 | color: rgba(255, 255, 255, 0.5); 325 | } 326 | .overview li.error { 327 | color: rgba(255, 50, 0, 0.7); 328 | } 329 | 330 | .overview li span { 331 | padding-right: 5px; 332 | } 333 | 334 | .grep { 335 | background: rgb(42, 44, 57); 336 | padding: 20px 0 100px 0; 337 | } 338 | 339 | .grep input { 340 | padding: 5px 10px; 341 | border: 0; 342 | outline: 0; 343 | width: 100%; 344 | background: #fff; 345 | font: normal 12px "pt mono"; 346 | text-transform: uppercase; 347 | letter-spacing: 1px; 348 | color: #333; 349 | } 350 | 351 | .grep .form { 352 | background: #fff; 353 | width: 490px; 354 | margin: 20px auto; 355 | font: 12px Helvetica, Arial, sans-serif; 356 | display: table; 357 | table-layout: fixed; 358 | overflow: hidden; 359 | padding: 5px; 360 | } 361 | 362 | .grep label { 363 | width: 55px; 364 | padding: 5px; 365 | color: #666; 366 | cursor: pointer; 367 | text-transform: uppercase; 368 | letter-spacing: 1px; 369 | } 370 | 371 | .grep label span { 372 | font: normal 12px "pt mono"; 373 | } 374 | 375 | .grep label { 376 | display: table-cell; 377 | } 378 | 379 | .pointer { 380 | cursor: pointer; 381 | } 382 | 383 | .center { 384 | display:-ms-flexbox; 385 | -ms-flex-pack:center; 386 | -ms-flex-align:center; 387 | 388 | display:-moz-box; 389 | -moz-box-pack:center; 390 | -moz-box-align:center; 391 | 392 | display:-webkit-box; 393 | -webkit-box-pack:center; 394 | -webkit-box-align:center; 395 | 396 | display:box; 397 | box-pack:center; 398 | box-align:center; 399 | } 400 | 401 | .clear { 402 | clear: both; 403 | } 404 | -------------------------------------------------------------------------------- /app/templates.js: -------------------------------------------------------------------------------- 1 | exports["code"] = "
{first-line-num}.{first-line-source}
\n
{second-line-num}.{second-line-source}
\n
{third-line-num}.{third-line-source}
\n" 2 | exports["custom-frame"] = "\n\n \n \n hello\n \n \n

Hello!

\n

This is a custom frame.

\n \n\n" 3 | exports["diff"] = "
\n Expected:{0}\n
\n
\n Actual:     {1}\n
\n" 4 | exports["error"] = "
  • \n {title}\n

    {name}

    \n
    {diff}
    \n
    {code}
    \n
    \n {stack}\n
    \n
  • \n" 5 | exports["frame"] = "\n" 6 | exports["layout"] = "
    \n
    \n \n \n \n \n \n
    \n
    \n
    \n
    \n\n
    \n
    \n Repeat\n {conn}\n Maximize Frame\n Minimize Frame\n
    \n
    \n\n
    \n \n
    \n\n
    \n
    \n \n \n
    \n
    \n" 7 | exports["overview"] = "\n" 8 | exports["pass"] = "
    \n

    {0} Passed Assertions.

    \n
    \n" 9 | exports["stack-line"] = "
    \n" 10 | exports["stack"] = "
    {0}
    \n" 11 | exports["test"] = "
  • \n {icon}\n {name}\n
  • \n" 12 | exports["waiting"] = "
    \n

    {message}

    \n
    \n" -------------------------------------------------------------------------------- /bin/prova: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli').run(); 4 | -------------------------------------------------------------------------------- /docs/examples: -------------------------------------------------------------------------------- 1 | 2 | EXAMPLES 3 | 4 | 1. Run the tests on NodeJS. 5 | 6 | $ node test.js 7 | $ node test 8 | $ prova test/index.js 9 | $ prova 10 | 11 | All the above example commands will work same way. Prova assumes the filename of your test is either `test.js` or `test/index.js` 12 | 13 | 2. Publish the tests on localhost:7559, so you can run the tests on a web browser. 14 | 15 | $ node test.js -b 16 | $ prova test -b 17 | $ prova -b 18 | 19 | 3. Publish the tests on given host and port. 20 | 21 | $ node test.js -o 8080 -d foobar.net 22 | $ prova test.js -p 8080 -d foobar.net 23 | 24 | 4. Publish the tests and launch a browser to automatically run the tests. 25 | 26 | $ node test.js -b -l chrome 27 | $ prova test.js -b -l chrome 28 | 29 | 5. List the browsers that can be launched automatically. 30 | 31 | $ prova -l 32 | $ node test.js -l 33 | 34 | 6. Run the tests with PhantomJS. 35 | 36 | $ node test.js -b -l phantom 37 | $ prova test.js -b -l phantom 38 | 39 | 7. Run only specified tests with PhantomJS. 40 | 41 | $ node test.js -b -l phantom -g pattern 42 | $ prova test.js -b -l phantom -g pattern 43 | 44 | 8. Launch Chrome headlessly using xvfb: 45 | 46 | $ node test -b -l chrome -e 47 | $ prova test -b -l chrome -e 48 | -------------------------------------------------------------------------------- /docs/man: -------------------------------------------------------------------------------- 1 | USAGE 2 | 3 | prova [filenames] [options] 4 | 5 | OPTIONS 6 | 7 | -g --grep Run tests matching with given pattern 8 | 9 | -b --browser Publishes the tests on 0.0.0.0:7559 10 | -o --port Publish the tests on given port number. 11 | -d --hostname Publish the tests on given hostname. 12 | -l --launch List available browsers to launch or launch specified browser. 13 | -e --headless Launch the browser headlessly. (Requires xvfb) 14 | -r --proxy Launch the browser with specified proxy configuration. 15 | -q --quit Shut down the browser server once all the tests are done. 16 | -f --frame Specify a custom document to run tests on browser. e.g node test -b -f custom.html 17 | -x --exec Execute given commmand before running the tests. 18 | -y --http-proxy Proxy requests matching with given pattern to a target. e.g -y "/images=localhost:8080" 19 | 20 | -t --transform Use given Browserify transforms. e.g node test -b -t coffeeify,brfs 21 | -u --plugin Use given Browserify plugins. e.g node test -b -u foo,bar 22 | 23 | -s --progress Show a progress bar. Useful when tests are running slow. 24 | 25 | -p --tap Output original Tap output without modifying anything. 26 | 27 | -C --no-console Disable showing browser console messages on command-line. 28 | 29 | -v --version Show version and exit 30 | -h --help Show help and exit 31 | --examples Show example commands 32 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var test = require('./'); 2 | 3 | test('just passing', function (assert) { 4 | assert.plan(1); 5 | assert.equal(true, true); 6 | }); 7 | 8 | test('a test with failing assertions', function (assert) { 9 | assert.plan(2); 10 | assert.equal('

    hello

    ', '

    hello

    '); 11 | assert.equal({ a: 1, b: 3 }, { a: 1 }); 12 | }); 13 | 14 | test('a test fails because of invalid planning', function (assert) { 15 | assert.plan(3); 16 | assert.ok(false); 17 | }); 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var isNode = require("is-node"); 3 | var refine = require("./lib/refine"); 4 | var command = require('./lib/command'); 5 | var isProvaFrame = !isNode && document; 6 | var nodeRequire = require; 7 | var view, tests; 8 | 9 | empty.skip = empty; 10 | empty.only = empty; 11 | 12 | if (command.launch === true) { 13 | nodeRequire('./lib/cli').launch(); 14 | module.exports = empty; 15 | return; 16 | } 17 | 18 | if (command.examples) { 19 | nodeRequire('./lib/cli').examples(); 20 | module.exports = empty; 21 | return; 22 | } 23 | 24 | if (command.browser) { 25 | nodeRequire('./lib/cli').launch(); 26 | nodeRequire('./lib/browser')([require.main.filename], command); 27 | } else if ((isNode || isProvaFrame) && !command.tap) { 28 | view = isNode ? nodeRequire('./lib/node-reporter') : require('./lib/browser-reporter'); 29 | tape.createStream({ objectMode: true }).pipe(refine()).pipe(view()); 30 | } 31 | 32 | if (isNode) { 33 | tests = require('./lib/tests'); 34 | } 35 | 36 | module.exports = prova; 37 | module.exports.skip = skip; 38 | module.exports.only = only; 39 | 40 | function prova (title, fn) { 41 | if (command.browser) return; 42 | if (command.grep && title.indexOf(command.grep) == -1) return skip(title, fn); 43 | if (isNode) tests.add(title); 44 | return tape(title, fn); 45 | } 46 | 47 | function skip (title, fn) { 48 | return tape.skip(title, fn); 49 | } 50 | 51 | function only (title, fn) { 52 | return tape.only(title, fn); 53 | } 54 | 55 | function empty () {} 56 | -------------------------------------------------------------------------------- /lib/browser-command.js: -------------------------------------------------------------------------------- 1 | var querystring = require("querystring"); 2 | var opts = querystring.parse(document.location.hash.slice(1), ';', ':'); 3 | 4 | module.exports = opts; 5 | -------------------------------------------------------------------------------- /lib/browser-reporter.js: -------------------------------------------------------------------------------- 1 | var through = require("through"); 2 | var filterStack = require("filter-stack"); 3 | var failingCode = require("failing-code"); 4 | var tests = {}; 5 | 6 | var started = false; 7 | var failed = 0; 8 | var passed = 0; 9 | var excludeFromStack = ['prova']; 10 | 11 | module.exports = reporter; 12 | 13 | function reporter () { 14 | return through(each, end); 15 | } 16 | 17 | function each (row) { 18 | var msg; 19 | 20 | if (!started) { 21 | post('start'); 22 | started = true; 23 | } 24 | 25 | if (row.type == 'assert' && row.ok) { 26 | passed++; 27 | } else if (row.type == 'assert') { 28 | tests[row.testName] = false; 29 | failed++; 30 | fail(row); 31 | } 32 | 33 | if (row.type == 'test') { 34 | tests[row.name] = true; 35 | post('test', row); 36 | } 37 | 38 | this.queue(row); 39 | } 40 | 41 | function fail (row) { 42 | var error = row.actual && row.actual.stack ? row.actual : row.error; 43 | var shift = error == row.actual ? 0 : 4; 44 | var failing = failingCode(error, __source_code, shift); 45 | 46 | var expected = row.expected == undefined ? '' : row.expected; 47 | var actual = row.actual == undefined ? '' : row.actual; 48 | 49 | if (error == row.actual) { 50 | expected = undefined; 51 | actual = undefined; 52 | } 53 | 54 | post('error', { 55 | test: row.testName, 56 | name: row.name, 57 | stack: error.stack || '', 58 | source: failing || [], 59 | expected: expected, 60 | actual: actual 61 | }); 62 | } 63 | 64 | function end () { 65 | post('end', { 66 | failed: failed, 67 | passed: passed, 68 | tests: tests 69 | }); 70 | 71 | this.queue && this.queue(null); 72 | } 73 | 74 | function post (type, msg) { 75 | msg || (msg = {}); 76 | msg.type = type; 77 | window.parent.postMessage(JSON.parse(JSON.stringify(msg)), document.location.origin); 78 | } 79 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | var debug = require("local-debug")('browser'); 2 | var http = require("http"); 3 | var browserify = require("browserify"); 4 | var routeMap = require("route-map"); 5 | var fs = require("fs"); 6 | var path = require("path"); 7 | var resumer = require("resumer"); 8 | var format = require("stream-format"); 9 | var formatText = require("format-text"); 10 | var concat = require("concat-stream"); 11 | var glob = require("glob").sync; 12 | var parseUserAgent = require("user-agent-parser"); 13 | var prettifyError = require("prettify-error"); 14 | var WebSocket = require("faye-websocket"); 15 | var mime = require("mime"); 16 | var request = require("request"); 17 | var setContentType = require("set-content-type"); 18 | var url = require("url"); 19 | 20 | var execFirst = require("./exec-first"); 21 | 22 | var nodeTemplate = require("./node-template"); 23 | var nodeReporter = require("./node-reporter"); 24 | var transforms = require('./browserify-transforms'); 25 | 26 | var passTemplate = nodeTemplate('browser-passed-result'); 27 | var failTemplate = nodeTemplate('browser-failed-result'); 28 | var browserTemplate = nodeTemplate('browser-error-browser'); 29 | var errorTemplate = nodeTemplate('browser-error'); 30 | var errorNoStackTemplate = nodeTemplate('browser-error-no-stack'); 31 | var consoleTemplate = nodeTemplate('browser-console'); 32 | var testTemplate = nodeTemplate('browser-test'); 33 | var instructionsTemplate = nodeTemplate('browser-instructions'); 34 | 35 | var onSourceCodeChange = require("pubsub")(); 36 | var customFrameURL, matchURL, read; 37 | 38 | var routes = { 39 | '/assets/app.js': app, 40 | '/assets/run.js': run, 41 | '/assets/:file': provaAsset, 42 | '/run': frame, 43 | '/': index 44 | }; 45 | 46 | var templates = glob(provaPath('../../templates/*.html')).map(function (f) { 47 | return { name: path.basename(f, '.html'), filename: f }; 48 | }); 49 | 50 | module.exports = beforeStart; 51 | 52 | function beforeStart (files, command) { 53 | if (!command.exec) return start(files, command); 54 | 55 | execFirst(command.exec, function () { 56 | start(files, command); 57 | }); 58 | } 59 | 60 | function start (files, command) { 61 | var http = require('http'); 62 | var server = http.createServer(route).listen(command.port, command.host); 63 | var restartNotification = JSON.stringify({ restart: true }); 64 | var sockets = []; 65 | 66 | read = transforms(files, command); 67 | read.on('update', onSourceCodeChange.publish); 68 | 69 | var customFrameFilename; 70 | var customFrameRoute; 71 | 72 | if (command.frame) { 73 | customFrameURL = path.join('/assets/in', command.frame); 74 | customFrameFilename = url.parse(command.frame).pathname; 75 | customFrameRoute = path.join('/assets/in', customFrameFilename); 76 | routes[customFrameRoute] = customFrame(customFrameFilename); 77 | } 78 | 79 | routes['/assets/in/:filename([\\w\\.\\/-]+)'] = localAsset; 80 | routes['/restart'] = restart; 81 | 82 | if (command['http-proxy']) { 83 | createProxy(routes, command['http-proxy']); 84 | } 85 | 86 | matchURL = routeMap(routes); 87 | 88 | server.on('upgrade', function(request, socket, body) { 89 | var ws; 90 | var ind; 91 | 92 | if (WebSocket.isWebSocket(request)) { 93 | ws = new WebSocket(request, socket, body); 94 | ind = sockets.push(ws) - 1; 95 | 96 | onSourceCodeChange.subscribe(notify); 97 | 98 | ws.send(JSON.stringify({ start: true, url: customFrameURL || '/run' })); 99 | ws.on('message', onMessage); 100 | ws.on('close', function(event) { 101 | ws = null; 102 | sockets[ind] = undefined; 103 | onSourceCodeChange.unsubscribe(notify); 104 | }); 105 | } 106 | 107 | function notify () { 108 | ws.send(restartNotification); 109 | } 110 | 111 | }); 112 | 113 | //var grep = command.grep ? '/#grep:' + command.grep : ''; 114 | //debug('Visit %s%s:%s%s with a browser to start running the tests.', 'http://', command.host, command.port, grep); 115 | outputInstructions(command); 116 | 117 | function onMessage (msg) { 118 | msg = JSON.parse(msg.data); 119 | 120 | var args = []; 121 | var key; 122 | if (msg.console && !command['no-console']) { 123 | outputConsole(msg.method, msg.params); 124 | } 125 | 126 | if (msg.test && command['progress']) { 127 | outputTest(msg.test, msg.userAgent); 128 | } 129 | 130 | if (msg.result) { 131 | outputResult(msg); 132 | if (command.quit) process.exit(msg.result.failed); 133 | return; 134 | } 135 | 136 | if (msg.fail) return outputError(msg); 137 | } 138 | 139 | function restart (request, response) { 140 | response.write('restartiiiinnnnggg....\n'); 141 | 142 | var i = 0; 143 | 144 | sockets.forEach(function (ws) { 145 | if (!ws) return; 146 | ws.send(restartNotification); 147 | i++; 148 | 149 | response.write(i + '\n'); 150 | }); 151 | 152 | response.end('done\n'); 153 | } 154 | } 155 | 156 | function route (req, res) { 157 | var match = matchURL(req.url); 158 | if (!match) return notfound().pipe(res); 159 | match.fn(req, res, match); 160 | }; 161 | 162 | function provaFile (file) { 163 | return fs.createReadStream(provaPath('../../app/' + file)); 164 | } 165 | 166 | function provaAsset (req, res, match) { 167 | setContentType(req, res); 168 | provaFile(match.params.file).on('error', function (error) { 169 | notfound().pipe(res); 170 | }).pipe(res); 171 | } 172 | 173 | function localAsset (req, res, match) { 174 | var filename = match.params.filename; 175 | setContentType(req, res); 176 | 177 | localFile(filename).on('error', function (error) { 178 | notfound().pipe(res); 179 | }).pipe(res); 180 | } 181 | 182 | function localFile (filename) { 183 | return fs.createReadStream(filename); 184 | } 185 | 186 | function index (req, res) { 187 | var waiting = format({ 188 | message: 'loading' 189 | }); 190 | 191 | fs.createReadStream(provaPath('../../templates/waiting.html')) 192 | .pipe(waiting); 193 | 194 | var render = format({ 195 | layout: waiting 196 | }); 197 | 198 | provaFile('index.html').pipe(render); 199 | render.pipe(res); 200 | } 201 | 202 | function build (filename) { 203 | var b = browserify({ debug: true }); 204 | b.add(filename); 205 | return b; 206 | } 207 | 208 | function app (req, res) { 209 | convertTemplates(); 210 | setContentType(req, res); 211 | build(provaPath('../../app/index.js')).bundle().pipe(res); 212 | } 213 | 214 | function run (req, res) { 215 | var sourcemaps; 216 | var ind; 217 | 218 | var write = concat(function (build) { 219 | var str = build.toString(); 220 | ind = str.indexOf('//# sourceMappingURL'); 221 | str = str.slice(0, ind) + '\n\nwindow.__source_code = window.parent.__source_code = ' + JSON.stringify(str.slice(0, ind)) + ';\n\n' + str.slice(ind); 222 | setContentType(req, res); 223 | res.end(str); 224 | }); 225 | 226 | read.bundle().on('error', function (error) { 227 | debug('Failed to browserify the source code. We\'re probably missing a module required. The error was:'); 228 | process.stderr.write('\n '); 229 | console.error(prettifyError(error, 0) || error); 230 | }).pipe(write); 231 | } 232 | 233 | function notfound () { 234 | return resumer().queue('Not Found').end(); 235 | } 236 | 237 | function frame (req, res) { 238 | provaFile('frame.html').pipe(res); 239 | } 240 | 241 | function convertTemplates () { 242 | var content = templates.map(function (template) { 243 | return { 244 | name: template.name, 245 | html: fs.readFileSync(template.filename).toString() 246 | }; 247 | }); 248 | 249 | content = content.map(function (template) { 250 | return 'exports["' + template.name + '"] = ' + JSON.stringify(template.html); 251 | }); 252 | 253 | fs.writeFileSync(provaPath('../../app/templates.js'), content.join('\n')); 254 | } 255 | 256 | function provaPath (p) { 257 | return path.join(__filename, p); 258 | } 259 | 260 | process.on('uncaughtException', function (error) { 261 | console.error('\n', prettifyError(error) || error); 262 | }); 263 | 264 | function outputConsole (method, params) { 265 | console.log(formatText(consoleTemplate, { 266 | 'method': method, 267 | 'message': params.join(' ') 268 | }).replace('\n', '')); 269 | } 270 | 271 | function outputError (msg) { 272 | var fail = msg.fail; 273 | var error = { 274 | message: fail.name, 275 | stack: fail.stack 276 | }; 277 | 278 | console.log(formatText(error.stack ? errorTemplate : errorNoStackTemplate, { 279 | browser: formatUserAgent(msg.userAgent), 280 | title: fail.test, 281 | diff: diff(fail), 282 | error: nodeReporter.tab(prettifyError(error, undefined, fail.source) || '', ' ') 283 | })); 284 | } 285 | 286 | function outputTest (name, userAgent) { 287 | console.log(formatText(testTemplate, { 288 | name: name, 289 | userAgent: formatUserAgent(userAgent) 290 | }).replace('\n', '')); 291 | } 292 | 293 | function outputResult (msg) { 294 | var failed = msg.result.failed; 295 | var template = failed ? failTemplate : passTemplate; 296 | 297 | console.log(formatText(template, msg.result) + ' ' + formatUserAgent(msg.userAgent)); 298 | } 299 | 300 | function outputInstructions (command) { 301 | var params = ''; 302 | 303 | if (command.grep) { 304 | params = '/#grep:' + command.grep; 305 | } 306 | 307 | console.log(formatText(instructionsTemplate, { 308 | host: command.host, 309 | port: command.port, 310 | params: params 311 | })); 312 | } 313 | 314 | function formatUserAgent (rawUserAgent) { 315 | var userAgent = parseUserAgent(rawUserAgent); 316 | 317 | return formatText(browserTemplate, { 318 | browser: userAgent.browser.name || '?', 319 | browserVersion: userAgent.browser.major || '?', 320 | engine: userAgent.engine.name || '?', 321 | engineVersion: userAgent.engine.version || '?', 322 | os: userAgent.os.name || '?', 323 | osVersion: userAgent.os.version || '?' 324 | }) 325 | } 326 | 327 | function diff (fail) { 328 | return nodeReporter.diff(fail).slice(1).split('\n').map(function (line) { 329 | return line.slice(4); 330 | }).join('\n'); 331 | } 332 | 333 | function customFrame (filename) { 334 | return function (req, res) { 335 | fs.readFile(filename, function (error, bf) { 336 | if (error) { 337 | debug('Error: Failed to read %s, specified as custom frame.', filename); 338 | console.error(error); 339 | return; 340 | }; 341 | 342 | var html = bf.toString(); 343 | html += ''; 344 | setContentType(req, res); 345 | res.end(html); 346 | }); 347 | }; 348 | } 349 | 350 | function createProxy (routes, param) { 351 | var options = param.split('='); 352 | var pattern = options[0]; 353 | var target = options[1]; 354 | 355 | routes[pattern] = onProxyRequest 356 | routes[pattern + '/'] = onProxyRequest 357 | routes[pattern + '/:filename([\\w\\.\\/-]+)'] = onProxyRequest 358 | 359 | function onProxyRequest (req, res, match) { 360 | var url = req.url.replace(pattern, ''); 361 | var method = req.method.toLowerCase() 362 | 363 | if (url[0] != '/') { 364 | url = '/' + url; 365 | } 366 | 367 | var proxy = request[method](target + url) 368 | req.pipe(proxy) 369 | proxy.pipe(res).on('error', function (error) { 370 | res.end(error.message); 371 | }); 372 | }; 373 | } 374 | -------------------------------------------------------------------------------- /lib/browserify-transforms.js: -------------------------------------------------------------------------------- 1 | var debug = require("local-debug")('browserify'); 2 | var extname = require('path').extname; 3 | var browserify = require('browserify'); 4 | var watchify = require('watchify'); 5 | var path = require("path"); 6 | 7 | var transformMap = { 8 | '.coffee': 'coffeeify', 9 | '.gs': 'gorillaify', 10 | '.iced': 'icsify', 11 | '.ls': 'liveify', 12 | '.coco': 'cocoify', 13 | '.ts': 'typescriptifier' 14 | }; 15 | 16 | module.exports = function (files, command) { 17 | var ext = extname(files[0]); 18 | var transform = ext != '.js'; 19 | var ret; 20 | 21 | var b = browserify({ 22 | debug: true, 23 | cache: {}, 24 | packageCache: {}, 25 | fullPaths: true 26 | }); 27 | 28 | b.add(files); 29 | b.bundle(); 30 | 31 | if (transform) { 32 | ret = watchify(b, { extensions: [ext, '.js', '.json'] }); 33 | } else { 34 | ret = watchify(b); 35 | } 36 | 37 | if (transform) ret.transform(transformMap[ext]); 38 | 39 | if (command.transform && command.transform.length) { 40 | command.transform.split(',').forEach(function (name) { 41 | if (!name) return; 42 | debug('Transform "%s" enabled', name); 43 | ret.transform(path.join(process.cwd(), 'node_modules', name)); 44 | }); 45 | } 46 | 47 | if (command.plugin && command.plugin.length) { 48 | command.plugin.split(',').forEach(function (name) { 49 | if (!name) return; 50 | debug('Plugin "%s" enabled', name); 51 | ret.plugin(require(path.join(process.cwd(), 'node_modules', name))); 52 | }); 53 | } 54 | 55 | return ret; 56 | }; 57 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | require('default-debug')('prova:browser,prova:launch,prova:exec-first'); 2 | 3 | var command = require('./node-command'); 4 | var path = require('path'); 5 | var glob = require('flat-glob'); 6 | var fs = require('fs'); 7 | var launch = require("../lib/launch"); 8 | var exists = fs.existsSync; 9 | var stats = fs.lstatSync; 10 | 11 | module.exports = { 12 | command: command, 13 | defaults: defaults, 14 | run: run, 15 | launch: launchBrowser, 16 | examples: examples 17 | }; 18 | 19 | function defaults () { 20 | if (!command.host) command.host = 'localhost'; 21 | if (!command.port) command.port = 7559; 22 | } 23 | 24 | function run () { 25 | defaults(); 26 | 27 | if (command.launch === true) return launch.list(); 28 | if (command.examples) return examples(); 29 | 30 | var files = (command._.length ? command._ : ['test']).map(function (p) { 31 | if (/^\w+$/.test(p) && exists(p + '.js') && stats(p + '.js').isFile()) { 32 | return p + '.js'; 33 | } 34 | 35 | if (/^\w+$/.test(p) && exists(p + '/index.js')) { 36 | return p + '/index.js'; 37 | } 38 | 39 | return p; 40 | }); 41 | 42 | glob(files, function (err, files) { 43 | (command.browser ? browser : node)(files); 44 | }); 45 | } 46 | 47 | function browser (files) { 48 | files = files.map(function (p) { 49 | return path.join(process.cwd(), p); 50 | }); 51 | 52 | if (command.launch) launchBrowser(); 53 | require('../lib/browser')(files, command); 54 | } 55 | 56 | function node (files) { 57 | if (command.launch) return; 58 | 59 | files.forEach(function (file) { 60 | require(path.resolve(process.cwd(), file)); 61 | }); 62 | } 63 | 64 | function launchBrowser () { 65 | if (!command.launch) return; 66 | if (typeof command.launch != 'string') return launch.list(); 67 | 68 | var url = 'http://' + command.host + ':' + command.port; 69 | 70 | if (command.grep) { 71 | url += '#grep:' + command.grep; 72 | } 73 | 74 | command.launch.split(',').forEach(function (browser) { 75 | launch(url, { 76 | browser: browser, 77 | headless: command.headless || false, 78 | proxy: command.proxy || undefined 79 | }); 80 | }); 81 | } 82 | 83 | function examples () { 84 | console.log(fs.readFileSync(path.join(__dirname, '../docs/examples')).toString()); 85 | } 86 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | var isNode = require("is-node"); 2 | var nodeRequire; 3 | var command; 4 | var cli; 5 | 6 | if (isNode) { 7 | nodeRequire = require; 8 | cli = nodeRequire('./cli'); 9 | cli.defaults(); 10 | command = cli.command; 11 | delete nodeRequire; 12 | } else { 13 | command = require('./browser-command'); 14 | } 15 | 16 | module.exports = command; 17 | -------------------------------------------------------------------------------- /lib/exec-first.js: -------------------------------------------------------------------------------- 1 | var debug = require("local-debug")('exec-first'); 2 | var exec = require("child_process").exec; 3 | 4 | module.exports = execFirst; 5 | 6 | function execFirst (command, callback) { 7 | debug('Calling "%s"', command); 8 | 9 | exec(command, function (error, stdout, stderr) { 10 | if (error) { 11 | throw error; 12 | } 13 | 14 | if (stdout) { 15 | debug('%s (stdout) > %s', command, stdout); 16 | } 17 | 18 | if (stderr) { 19 | debug('%s (stderr) > %s', command, stderr); 20 | } 21 | 22 | callback(); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /lib/launch.js: -------------------------------------------------------------------------------- 1 | var debug = require("local-debug")('launch'); 2 | var launcher = require("browser-launcher"); 3 | var style = require("style-format"); 4 | var template = require("./node-template")('browser-launch'); 5 | var format = require("format-text"); 6 | 7 | module.exports = launch; 8 | module.exports.list = list; 9 | 10 | function list () { 11 | launcher.detect(function (avail) { 12 | console.log(style('\n {cyan}Available Browsers:\n{reset} ' + avail.map(function (b) { 13 | return '\n ' + b.name + ' {grey}v' + b.version + '{reset}'; 14 | }).join('')) + '\n'); 15 | }); 16 | } 17 | 18 | function launch (url, options) { 19 | launcher(function (error, start) { 20 | if (error) return console.error(error); 21 | 22 | start(url, options, function (error, ps) { 23 | if (error) return console.error(error); 24 | 25 | process.on('exit', function(code) { 26 | ps.kill('SIGTERM'); 27 | }); 28 | 29 | var extras = ''; 30 | if (options.headless) extras += ' headlessly'; 31 | if (options.proxy) extras += ' through ' + options.proxy; 32 | 33 | console.log(format(template, { 34 | app: options.browser, 35 | url: url, 36 | extras: extras 37 | })); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /lib/node-command.js: -------------------------------------------------------------------------------- 1 | module.exports = require("new-command")({ 2 | l: 'launch', 3 | g: 'grep', 4 | b: 'browser', 5 | e: 'headless', 6 | r: 'proxy', 7 | o: 'port', 8 | d: 'host', 9 | q: 'quit', 10 | t: 'transform', 11 | u: 'plugin', 12 | m: 'examples', 13 | f: 'frame', 14 | x: 'exec', 15 | s: 'progress', 16 | p: 'tap', 17 | y: 'http-proxy', 18 | C: 'no-console' 19 | }); 20 | -------------------------------------------------------------------------------- /lib/node-reporter.js: -------------------------------------------------------------------------------- 1 | var through = require("through"); 2 | var format = require("format-text"); 3 | var filterStack = require("filter-stack"); 4 | var prettifyError = require("prettify-error"); 5 | var template = require("./node-template"); 6 | 7 | var failTemplate = template('fail'); 8 | var resultPassTemplate = template('result-pass'); 9 | var resultTemplate = template('result'); 10 | var diffTemplate = template('diff'); 11 | var command = require("./node-command"); 12 | 13 | var progress = require('./progress'); 14 | var tests = require("./tests"); 15 | 16 | var failed = 0; 17 | var passed = 0; 18 | var excludeFromStack = ['prova']; 19 | 20 | module.exports = reporter; 21 | module.exports.diff = diff; 22 | module.exports.tab = tab; 23 | 24 | function reporter () { 25 | return through(each, end); 26 | } 27 | 28 | function each (row) { 29 | if (row.type == 'assert' && row.ok) { 30 | passed++; 31 | } else if (row.type == 'assert') { 32 | failed++; 33 | fail(row); 34 | } 35 | 36 | if (command.progress && row.type == 'test') { 37 | tests.done(row.name); 38 | progress.show(row); 39 | } 40 | } 41 | 42 | function fail (row) { 43 | var error = row.actual && row.actual.stack ? row.actual : row.error; 44 | error.stack = filterStack(error, excludeFromStack).stack; 45 | 46 | var startingLine = 0; 47 | var stackLines = error.stack.split('\n'); 48 | var len = stackLines.length; 49 | var found = false; 50 | while (++startingLine < len) { 51 | if (/node_modules\/tape\//.test(stackLines[startingLine])) continue; 52 | found = true; 53 | break; 54 | } 55 | 56 | if (!found) startingLine = 1; 57 | 58 | var prettifiedError = prettifyError(error, startingLine - 1); 59 | 60 | if (!prettifiedError) { 61 | prettifiedError = '\n ' + error.message + '\n' + tab(error.stack, ' '); 62 | } 63 | 64 | console.error(format(failTemplate, { 65 | title: row.testName, 66 | diff: diff(row), 67 | error: tab(prettifiedError, ' ') 68 | })); 69 | } 70 | 71 | function diff (row) { 72 | if (!row.hasOwnProperty('expected')) return ''; 73 | return format(diffTemplate, JSON.stringify(row.expected), JSON.stringify(row.actual)) + '\n'; 74 | } 75 | 76 | function end () { 77 | (command.progress ? progress.end : console.log)(format(failed ? resultTemplate : resultPassTemplate, passed, failed)); 78 | 79 | this.queue(null); 80 | 81 | if (command.quit) { 82 | process.exit(failed); 83 | } 84 | } 85 | 86 | function tab (text, ch) { 87 | return ch + text.replace(/\n/g, '\n' + ch); 88 | } 89 | -------------------------------------------------------------------------------- /lib/node-template.js: -------------------------------------------------------------------------------- 1 | var style = require("style-format"); 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | 5 | module.exports = template; 6 | 7 | function template (name) { 8 | var filename = path.join(__filename, "../../templates/node-" + name + ".txt"); 9 | return style(fs.readFileSync(filename).toString()); 10 | } 11 | -------------------------------------------------------------------------------- /lib/progress.js: -------------------------------------------------------------------------------- 1 | var tests = require("./tests"); 2 | var style = require("style-format"); 3 | var format = require("format-text"); 4 | var template = style(' {bold}{percent}%:{reset} {test}\n'); 5 | var log; 6 | 7 | module.exports = { 8 | init: init, 9 | show: show, 10 | end: end 11 | }; 12 | 13 | function init () { 14 | log = require('single-line-log').stdout; 15 | console.log(''); 16 | } 17 | 18 | function show (row) { 19 | if (!log) init(); 20 | 21 | var remaining = tests.remaining(); 22 | var done = tests.count() - remaining.length; 23 | var percent = !done ? 1 : Math.round((done * 100) / tests.count()); 24 | var current = remaining[0]; 25 | 26 | log(format(template, { 27 | percent: percent, 28 | test: current || 'Done!' 29 | })); 30 | } 31 | 32 | function end (msg) { 33 | log((msg || '').replace(/^\n/, '') + '\n'); 34 | } 35 | -------------------------------------------------------------------------------- /lib/refine.js: -------------------------------------------------------------------------------- 1 | var through = require("through"); 2 | var tests = {}; 3 | 4 | module.exports = refine; 5 | module.exports.tests = tests; 6 | 7 | function refine () { 8 | return through(map); 9 | } 10 | 11 | function map (row) { 12 | if (row.type == 'end') { 13 | this.queue(row); 14 | return; 15 | } 16 | 17 | if (row.type == 'test') { 18 | tests[row.id] = row.name; 19 | } 20 | 21 | if (row.type != 'assert' || row.ok) { 22 | this.queue(row); 23 | return; 24 | } 25 | 26 | row.testName = tests[row.test]; 27 | 28 | this.queue(row); 29 | } 30 | -------------------------------------------------------------------------------- /lib/tests.js: -------------------------------------------------------------------------------- 1 | var tests = []; 2 | var state = {}; 3 | 4 | module.exports = { 5 | add: add, 6 | get: get, 7 | count: count, 8 | done: done, 9 | remaining: remaining 10 | }; 11 | 12 | function get () { 13 | return tests; 14 | } 15 | 16 | function add (name) { 17 | tests.push(name); 18 | } 19 | 20 | function count () { 21 | return tests.length; 22 | } 23 | 24 | function done (name) { 25 | state[name] = true; 26 | } 27 | 28 | function remaining () { 29 | return tests.filter(function (name) { 30 | return !state[name]; 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prova", 3 | "version": "2.1.2", 4 | "description": "Test runner based on Tape and Browserify", 5 | "main": "index.js", 6 | "bin": { 7 | "prova": "./bin/prova" 8 | }, 9 | "scripts": { 10 | "test": "prova test/{index,multiple}.js" 11 | }, 12 | "dependencies": { 13 | "browser-launcher": "^1.0.0", 14 | "browserify": "^6.2.0", 15 | "component-delegate": "^0.2.3", 16 | "concat-stream": "^1.4.4", 17 | "default-debug": "0.0.0", 18 | "dom-classes": "0.0.1", 19 | "dom-event": "0.0.1", 20 | "dom-select": "0.0.0", 21 | "dom-style": "0.0.3", 22 | "dom-tree": "0.0.1", 23 | "escape-html": "^1.0.1", 24 | "failing-code": "azer/failing-code", 25 | "faye-websocket": "^0.7.3", 26 | "filter-stack": "azer/filter-stack", 27 | "flat-glob": "0.0.1", 28 | "format-text": "azer/format-text", 29 | "glob": "^4.0.6", 30 | "is-node": "azer/is-node", 31 | "key-event": "azer/key-event", 32 | "left-pad": "0.0.3", 33 | "local-debug": "0.0.0", 34 | "mime": "^1.2.11", 35 | "new-command": "azer/new-command", 36 | "prettify-error": "azer/prettify-error", 37 | "pubsub": "azer/pubsub", 38 | "request": "^2.48.0", 39 | "resumer": "0.0.0", 40 | "route-map": "^0.1.0", 41 | "run-serially": "azer/run-serially", 42 | "set-content-type": "azer/set-content-type", 43 | "single-line-log": "^0.4.1", 44 | "stream-format": "azer/stream-format", 45 | "style-format": "azer/style-format", 46 | "tape": "^3.0.0", 47 | "through": "~2.3.4", 48 | "user-agent-parser": "^0.6.0", 49 | "watchify": "^2.1.1" 50 | }, 51 | "keywords": [ 52 | "testing", 53 | "test", 54 | "tap", 55 | "tape" 56 | ], 57 | "repository": { 58 | "url": "https://github.com/azer/prova.git", 59 | "type": "git" 60 | }, 61 | "author": "azer", 62 | "license": "BSD" 63 | } 64 | -------------------------------------------------------------------------------- /templates/code.html: -------------------------------------------------------------------------------- 1 |
    {first-line-num}.{first-line-source}
    2 |
    {second-line-num}.{second-line-source}
    3 |
    {third-line-num}.{third-line-source}
    4 | -------------------------------------------------------------------------------- /templates/custom-frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello 6 | 7 | 8 |

    Hello!

    9 |

    This is a custom frame.

    10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/diff.html: -------------------------------------------------------------------------------- 1 |
    2 | Expected:{0} 3 |
    4 |
    5 | Actual:     {1} 6 |
    7 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {title} 3 |

    {name}

    4 |
    {diff}
    5 |
    {code}
    6 |
    7 | {stack} 8 |
    9 |
  • 10 | -------------------------------------------------------------------------------- /templates/frame.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 | 7 |
      8 |
      9 |
      10 |
      11 |
      12 | 13 | 21 | 22 |
      23 | 24 |
      25 | 26 |
      27 |
      28 | 29 | 30 |
      31 |
      32 | -------------------------------------------------------------------------------- /templates/node-browser-console.txt: -------------------------------------------------------------------------------- 1 | {reset}{grey}console:{method} > {white}{message}{reset} 2 | -------------------------------------------------------------------------------- /templates/node-browser-error-browser.txt: -------------------------------------------------------------------------------- 1 | {reset}{grey}{bold}Browser:{reset} {grey}{browser} v{browserVersion} {bold}Engine:{reset} {grey}{engine} v{engineVersion} {bold}OS:{reset} {grey}{os} v{osVersion} 2 | -------------------------------------------------------------------------------- /templates/node-browser-error-no-stack.txt: -------------------------------------------------------------------------------- 1 | 2 | {bold}{title}{reset} 3 | {grey}{browser}{reset} 4 | {diff} 5 | -------------------------------------------------------------------------------- /templates/node-browser-error.txt: -------------------------------------------------------------------------------- 1 | 2 | {reset}{bold}{title}{reset} 3 | {grey}{browser}{reset} 4 | {error} 5 | {diff} 6 | -------------------------------------------------------------------------------- /templates/node-browser-failed-result.txt: -------------------------------------------------------------------------------- 1 | {reset} 2 | {red}{failed} failed and {passed} passed assertions. 3 | -------------------------------------------------------------------------------- /templates/node-browser-instructions.txt: -------------------------------------------------------------------------------- 1 | {reset} 2 | {cyan}Visit {reset}{bold}http://{host}:{port}{params}{grey} to run the tests 3 | -------------------------------------------------------------------------------- /templates/node-browser-launch.txt: -------------------------------------------------------------------------------- 1 | {reset} {grey}Launching {reset}{bold}{app} {grey}to run the tests{extras} 2 | -------------------------------------------------------------------------------- /templates/node-browser-passed-result.txt: -------------------------------------------------------------------------------- 1 | {reset} 2 | {green}{passed} passed assertions. 3 | -------------------------------------------------------------------------------- /templates/node-browser-test.txt: -------------------------------------------------------------------------------- 1 | {reset}{grey}running > {white}{name}{reset} 2 | -------------------------------------------------------------------------------- /templates/node-browser.txt: -------------------------------------------------------------------------------- 1 | {reset}{bold}Browser:{reset} {browser} v{browserVersion} {bold}Engine:{reset} {engine} v{engineVersion} {bold}OS:{reset} {os} v{osVersion} 2 | -------------------------------------------------------------------------------- /templates/node-diff.txt: -------------------------------------------------------------------------------- 1 | 2 | {grey}{bold}Expected:{reset} {0} 3 | {grey}{bold}Actual:{reset} {1} 4 | -------------------------------------------------------------------------------- /templates/node-fail.txt: -------------------------------------------------------------------------------- 1 | 2 | {bold}{title}{reset} 3 | {error} 4 | {diff} 5 | -------------------------------------------------------------------------------- /templates/node-result-pass.txt: -------------------------------------------------------------------------------- 1 | {reset} 2 | {0} passed assertions. 3 | -------------------------------------------------------------------------------- /templates/node-result.txt: -------------------------------------------------------------------------------- 1 | {reset} 2 | {1} failed, {0} passed assertions. 3 | -------------------------------------------------------------------------------- /templates/overview.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/pass.html: -------------------------------------------------------------------------------- 1 |
      2 |

      {0} Passed Assertions.

      3 |
      4 | -------------------------------------------------------------------------------- /templates/stack-line.html: -------------------------------------------------------------------------------- 1 |
      2 | -------------------------------------------------------------------------------- /templates/stack.html: -------------------------------------------------------------------------------- 1 |
      {0}
      2 | -------------------------------------------------------------------------------- /templates/test.html: -------------------------------------------------------------------------------- 1 |
    • 2 | {icon} 3 | {name} 4 |
    • 5 | -------------------------------------------------------------------------------- /templates/waiting.html: -------------------------------------------------------------------------------- 1 |
      2 |

      {message}

      3 |
      4 | -------------------------------------------------------------------------------- /test/altjs/test.coffee: -------------------------------------------------------------------------------- 1 | test = require '../../' 2 | 3 | test 'assert object', (t)-> 4 | t.plan 4 5 | t.equal 3.14, 3.14 6 | t.ok false 7 | t.notOk false 8 | t.deepEqual [3, 1, 4], [3, 1, 4] 9 | -------------------------------------------------------------------------------- /test/altjs/test.gs: -------------------------------------------------------------------------------- 1 | let test = require './' 2 | 3 | test 'assert object', #(t) 4 | t.plan 4 5 | t.equal 3.14, 3.14 6 | t.ok false 7 | t.not-ok false 8 | t.deep-equal [3, 1, 4], [3, 1, 4] 9 | -------------------------------------------------------------------------------- /test/custom-frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | holaaa! 6 | 9 | 10 | 11 |

      Hola!

      12 |

      node test -b -f test/custom-frame.html

      13 |

      14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | var test = require("../"); 2 | 3 | test('throws an error', function (t) { 4 | err++; 5 | }); 6 | 7 | test('same as the other one', function (t) { 8 | err++; 9 | }); 10 | 11 | test('foo bar', function (t) { 12 | fail++ 13 | }); 14 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require("../"); 2 | 3 | test('assert object', function (assert) { 4 | assert.plan(4); 5 | assert.equal(3.14, 3.14); 6 | assert.ok(true); 7 | assert.notOk(false); 8 | assert.deepEqual([3, 1, 4], [3, 1, 4]); 9 | }); 10 | 11 | test.skip('skipping a test failing', function (assert) { 12 | assert.plan(3); 13 | assert.ok(false); 14 | }); 15 | -------------------------------------------------------------------------------- /test/multiple.js: -------------------------------------------------------------------------------- 1 | var test = require('../'); 2 | 3 | test('multiple tests', function (assert) { 4 | assert.ok(true); 5 | assert.end(); 6 | }); 7 | 8 | test('another case', function (assert) { 9 | assert.ok(true); 10 | assert.notOk(false); 11 | assert.end(); 12 | }); 13 | -------------------------------------------------------------------------------- /test/slow-errors.js: -------------------------------------------------------------------------------- 1 | var test = require("../"); 2 | 3 | test('such slow', function (t) { 4 | t.plan(1); 5 | 6 | setTimeout(function () { 7 | t.ok(0); 8 | }, 1000); 9 | }); 10 | 11 | test('so mystery', function (t) { 12 | t.plan(1); 13 | 14 | setTimeout(function () { 15 | t.ok(0); 16 | }, 1000); 17 | }); 18 | 19 | test('how steady', function (t) { 20 | t.plan(1); 21 | 22 | setTimeout(function () { 23 | t.ok(0); 24 | }, 1000); 25 | }); 26 | 27 | test('very unsure', function (t) { 28 | t.plan(1); 29 | 30 | setTimeout(function () { 31 | t.ok(0); 32 | }, 1000); 33 | }); 34 | 35 | test('wow', function (t) { 36 | t.plan(1); 37 | 38 | setTimeout(function () { 39 | t.ok(0); 40 | }, 1500); 41 | }); 42 | -------------------------------------------------------------------------------- /test/slow.js: -------------------------------------------------------------------------------- 1 | var test = require("../"); 2 | 3 | test('such slow', function (t) { 4 | t.plan(1); 5 | 6 | setTimeout(function () { 7 | t.ok(1); 8 | }, 1000); 9 | }); 10 | 11 | test('so mystery', function (t) { 12 | t.plan(1); 13 | 14 | setTimeout(function () { 15 | t.ok(1); 16 | }, 1000); 17 | }); 18 | 19 | test('how steady', function (t) { 20 | t.plan(1); 21 | 22 | setTimeout(function () { 23 | t.ok(1); 24 | }, 1000); 25 | }); 26 | 27 | test('very unsure', function (t) { 28 | t.plan(1); 29 | 30 | setTimeout(function () { 31 | t.ok(1); 32 | }, 1000); 33 | }); 34 | 35 | test('wow', function (t) { 36 | t.plan(1); 37 | 38 | setTimeout(function () { 39 | t.ok(1); 40 | }, 1500); 41 | }); 42 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | test = require "../" 2 | 3 | test "written in cs", (assert) -> 4 | assert.ok true 5 | assert.notOk false 6 | assert.end() 7 | 8 | test.skip "skipping a test failing", (assert) -> 9 | assert.notOk true 10 | assert.end() 11 | --------------------------------------------------------------------------------