├── .github └── workflows │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── isaacs-makework.yml │ └── package-json-repo.js ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── browser │ ├── debug.js │ ├── escape-string-regexp.js │ ├── events.js │ ├── fs.js │ ├── glob.js │ ├── path.js │ ├── progress.js │ └── tty.js ├── formatter.js ├── ms.js ├── reporters │ ├── base.js │ ├── classic.js │ ├── doc.js │ ├── dot.js │ ├── dump.js │ ├── index.js │ ├── json-stream.js │ ├── json.js │ ├── landing.js │ ├── list.js │ ├── markdown.js │ ├── min.js │ ├── nyan.js │ ├── progress.js │ ├── silent.js │ ├── spec.js │ ├── templates │ │ ├── coverage.jade │ │ ├── menu.jade │ │ ├── script.html │ │ └── style.html │ └── xunit.js ├── runner.js ├── suite.js ├── test.js └── utils.js ├── package-lock.json ├── package.json └── test └── index.js /.github/workflows/commit-if-modified.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.email "$1" 3 | shift 4 | git config --global user.name "$1" 5 | shift 6 | message="$1" 7 | shift 8 | if [ $(git status --porcelain "$@" | egrep '^ M' | wc -l) -gt 0 ]; then 9 | git add "$@" 10 | git commit -m "$message" 11 | git push || git pull --rebase 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /.github/workflows/copyright-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=${1:-$PWD} 3 | dates=($(git log --date=format:%Y --pretty=format:'%ad' --reverse | sort | uniq)) 4 | if [ "${#dates[@]}" -eq 1 ]; then 5 | datestr="${dates}" 6 | else 7 | datestr="${dates}-${dates[${#dates[@]}-1]}" 8 | fi 9 | 10 | stripDate='s/^((.*)Copyright\b(.*?))((?:,\s*)?(([0-9]{4}\s*-\s*[0-9]{4})|(([0-9]{4},\s*)*[0-9]{4})))(?:,)?\s*(.*)\n$/$1$9\n/g' 11 | addDate='s/^.*Copyright(?:\s*\(c\))? /Copyright \(c\) '$datestr' /g' 12 | for l in $dir/LICENSE*; do 13 | perl -pi -e "$stripDate" $l 14 | perl -pi -e "$addDate" $l 15 | done 16 | -------------------------------------------------------------------------------- /.github/workflows/isaacs-makework.yml: -------------------------------------------------------------------------------- 1 | name: "various tidying up tasks to silence nagging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | makework: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2.1.4 18 | with: 19 | node-version: 16.x 20 | - name: put repo in package.json 21 | run: node .github/workflows/package-json-repo.js 22 | - name: check in package.json if modified 23 | run: | 24 | bash -x .github/workflows/commit-if-modified.sh \ 25 | "package-json-repo-bot@example.com" \ 26 | "package.json Repo Bot" \ 27 | "chore: add repo to package.json" \ 28 | package.json package-lock.json 29 | - name: put all dates in license copyright line 30 | run: bash .github/workflows/copyright-year.sh 31 | - name: check in licenses if modified 32 | run: | 33 | bash .github/workflows/commit-if-modified.sh \ 34 | "license-year-bot@example.com" \ 35 | "License Year Bot" \ 36 | "chore: add copyright year to license" \ 37 | LICENSE* 38 | -------------------------------------------------------------------------------- /.github/workflows/package-json-repo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pf = require.resolve(`${process.cwd()}/package.json`) 4 | const pj = require(pf) 5 | 6 | if (!pj.repository && process.env.GITHUB_REPOSITORY) { 7 | const fs = require('fs') 8 | const server = process.env.GITHUB_SERVER_URL || 'https://github.com' 9 | const repo = `${server}/${process.env.GITHUB_REPOSITORY}` 10 | pj.repository = repo 11 | const json = fs.readFileSync(pf, 'utf8') 12 | const match = json.match(/^\s*\{[\r\n]+([ \t]*)"/) 13 | const indent = match[1] 14 | const output = JSON.stringify(pj, null, indent || 2) + '\n' 15 | fs.writeFileSync(pf, output) 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | .nyc_output 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2015-2023 Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | ---- 18 | 19 | The reporters in this package are based on Mocha, in accordance with the 20 | terms of Mocha's license: 21 | 22 | (The MIT License) 23 | 24 | Copyright (c) 2015-2023 TJ Holowaychuk 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining 27 | a copy of this software and associated documentation files (the 28 | 'Software'), to deal in the Software without restriction, including 29 | without limitation the rights to use, copy, modify, merge, publish, 30 | distribute, sublicense, and/or sell copies of the Software, and to 31 | permit persons to whom the Software is furnished to do so, subject to 32 | the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be 35 | included in all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 38 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 39 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 40 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 41 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 42 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 43 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tap-mocha-reporter 2 | 3 | Format a TAP stream using Mocha's set of reporters 4 | 5 | ## USAGE 6 | 7 | On the command line, pipe TAP in, and it'll do its thing. 8 | 9 | ```bash 10 | tap test/*.js | tap-mocha-reporter 11 | ``` 12 | 13 | You can also specify a reporter with the first argument. The default 14 | is `spec`. 15 | 16 | ```bash 17 | tap test/*.js | tap-mocha-reporter nyan 18 | ``` 19 | 20 | Programmatically, you can use this as a transform stream. 21 | 22 | ```javascript 23 | var TSR = require('tap-mocha-reporter') 24 | 25 | fs.createReadStream('saved-test-output.tap') 26 | .pipe(TSR('dot')) 27 | ``` 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | module.exports = Formatter 4 | 5 | var util = require('util') 6 | var reporters = require('./lib/reporters/index.js') 7 | Formatter.types = Object.keys(reporters).sort() 8 | var Writable = require('stream').Writable 9 | 10 | var Runner = require('./lib/runner.js') 11 | var Parser = require('tap-parser') 12 | 13 | util.inherits(Formatter, Writable) 14 | 15 | var exitCode 16 | function Formatter (type, options) { 17 | if (!(this instanceof Formatter)) { 18 | return new Formatter(type, options) 19 | } 20 | if (!reporters[type]) { 21 | console.error('Unknown format type: %s\n\n%s', type, avail()) 22 | type = 'silent' 23 | } 24 | 25 | this.writable = true 26 | 27 | // don't actually need a reporter to report the tap we're getting 28 | // just parse it so that we exit with the correct code, but otherwise 29 | // dump it straight through to stdout. 30 | if (type === 'tap') { 31 | var p = new Parser() 32 | this.write = function (chunk) { 33 | process.stdout.write(chunk) 34 | return p.write(chunk) 35 | } 36 | this.end = p.end.bind(p) 37 | p.on('complete', function () { 38 | if (!p.ok) 39 | exitCode = 1 40 | }) 41 | return this 42 | } 43 | 44 | var runner = this.runner = new Runner(options) 45 | this.reporter = new reporters[type](this.runner, {}) 46 | Writable.call(this, options) 47 | 48 | runner.on('end', function () { 49 | if (!runner.parser.ok) 50 | exitCode = 1 51 | }) 52 | } 53 | 54 | process.on('exit', function (code) { 55 | if (!code && exitCode) 56 | process.exit(exitCode) 57 | }) 58 | 59 | Formatter.prototype.write = function () { 60 | return this.runner.write.apply(this.runner, arguments) 61 | } 62 | 63 | Formatter.prototype.end = function () { 64 | return this.runner.end.apply(this.runner, arguments) 65 | } 66 | 67 | function avail () { 68 | var types = Formatter.types.reduce(function (str, t) { 69 | var ll = str.split('\n').pop().length + t.length 70 | if (ll < 40) 71 | return str + ' ' + t 72 | else 73 | return str + '\n' + t 74 | }, '').trim() 75 | 76 | return 'Available format types:\n\n' + types 77 | } 78 | 79 | 80 | function usage (err) { 81 | console[err ? 'error' : 'log'](function () {/* 82 | Usage: 83 | tap-mocha-reporter 84 | 85 | Reads TAP data on stdin, and formats to stdout using the specified 86 | reporter. (Note that some reporters write to files instead of stdout.) 87 | 88 | %s 89 | */}.toString().split('\n').slice(1, -1).join('\n'), avail()) 90 | } 91 | 92 | if (require.main === module) { 93 | var type = process.argv[2] 94 | if (!type) 95 | return usage() 96 | 97 | process.stdin.pipe(new Formatter(type)) 98 | } 99 | -------------------------------------------------------------------------------- /lib/browser/debug.js: -------------------------------------------------------------------------------- 1 | module.exports = function(type){ 2 | return function(){ 3 | } 4 | }; 5 | -------------------------------------------------------------------------------- /lib/browser/escape-string-regexp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; 4 | 5 | module.exports = function (str) { 6 | if (typeof str !== 'string') { 7 | throw new TypeError('Expected a string'); 8 | } 9 | 10 | return str.replace(matchOperatorsRe, '\\$&'); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/browser/events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module exports. 3 | */ 4 | 5 | exports.EventEmitter = EventEmitter; 6 | 7 | /** 8 | * Check if `obj` is an array. 9 | */ 10 | 11 | function isArray(obj) { 12 | return '[object Array]' == {}.toString.call(obj); 13 | } 14 | 15 | /** 16 | * Event emitter constructor. 17 | * 18 | * @api public 19 | */ 20 | 21 | function EventEmitter(){}; 22 | 23 | /** 24 | * Adds a listener. 25 | * 26 | * @api public 27 | */ 28 | 29 | EventEmitter.prototype.on = function (name, fn) { 30 | if (!this.$events) { 31 | this.$events = {}; 32 | } 33 | 34 | if (!this.$events[name]) { 35 | this.$events[name] = fn; 36 | } else if (isArray(this.$events[name])) { 37 | this.$events[name].push(fn); 38 | } else { 39 | this.$events[name] = [this.$events[name], fn]; 40 | } 41 | 42 | return this; 43 | }; 44 | 45 | EventEmitter.prototype.addListener = EventEmitter.prototype.on; 46 | 47 | /** 48 | * Adds a volatile listener. 49 | * 50 | * @api public 51 | */ 52 | 53 | EventEmitter.prototype.once = function (name, fn) { 54 | var self = this; 55 | 56 | function on () { 57 | self.removeListener(name, on); 58 | fn.apply(this, arguments); 59 | }; 60 | 61 | on.listener = fn; 62 | this.on(name, on); 63 | 64 | return this; 65 | }; 66 | 67 | /** 68 | * Removes a listener. 69 | * 70 | * @api public 71 | */ 72 | 73 | EventEmitter.prototype.removeListener = function (name, fn) { 74 | if (this.$events && this.$events[name]) { 75 | var list = this.$events[name]; 76 | 77 | if (isArray(list)) { 78 | var pos = -1; 79 | 80 | for (var i = 0, l = list.length; i < l; i++) { 81 | if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { 82 | pos = i; 83 | break; 84 | } 85 | } 86 | 87 | if (pos < 0) { 88 | return this; 89 | } 90 | 91 | list.splice(pos, 1); 92 | 93 | if (!list.length) { 94 | delete this.$events[name]; 95 | } 96 | } else if (list === fn || (list.listener && list.listener === fn)) { 97 | delete this.$events[name]; 98 | } 99 | } 100 | 101 | return this; 102 | }; 103 | 104 | /** 105 | * Removes all listeners for an event. 106 | * 107 | * @api public 108 | */ 109 | 110 | EventEmitter.prototype.removeAllListeners = function (name) { 111 | if (name === undefined) { 112 | this.$events = {}; 113 | return this; 114 | } 115 | 116 | if (this.$events && this.$events[name]) { 117 | this.$events[name] = null; 118 | } 119 | 120 | return this; 121 | }; 122 | 123 | /** 124 | * Gets all listeners for a certain event. 125 | * 126 | * @api public 127 | */ 128 | 129 | EventEmitter.prototype.listeners = function (name) { 130 | if (!this.$events) { 131 | this.$events = {}; 132 | } 133 | 134 | if (!this.$events[name]) { 135 | this.$events[name] = []; 136 | } 137 | 138 | if (!isArray(this.$events[name])) { 139 | this.$events[name] = [this.$events[name]]; 140 | } 141 | 142 | return this.$events[name]; 143 | }; 144 | 145 | /** 146 | * Emits an event. 147 | * 148 | * @api public 149 | */ 150 | 151 | EventEmitter.prototype.emit = function (name) { 152 | if (!this.$events) { 153 | return false; 154 | } 155 | 156 | var handler = this.$events[name]; 157 | 158 | if (!handler) { 159 | return false; 160 | } 161 | 162 | var args = [].slice.call(arguments, 1); 163 | 164 | if ('function' == typeof handler) { 165 | handler.apply(this, args); 166 | } else if (isArray(handler)) { 167 | var listeners = handler.slice(); 168 | 169 | for (var i = 0, l = listeners.length; i < l; i++) { 170 | listeners[i].apply(this, args); 171 | } 172 | } else { 173 | return false; 174 | } 175 | 176 | return true; 177 | }; 178 | -------------------------------------------------------------------------------- /lib/browser/fs.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tapjs/tap-mocha-reporter/61fe10acc2a7297ae14dfc74eb055d0d1ff89164/lib/browser/fs.js -------------------------------------------------------------------------------- /lib/browser/glob.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tapjs/tap-mocha-reporter/61fe10acc2a7297ae14dfc74eb055d0d1ff89164/lib/browser/glob.js -------------------------------------------------------------------------------- /lib/browser/path.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tapjs/tap-mocha-reporter/61fe10acc2a7297ae14dfc74eb055d0d1ff89164/lib/browser/path.js -------------------------------------------------------------------------------- /lib/browser/progress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Expose `Progress`. 3 | */ 4 | 5 | module.exports = Progress; 6 | 7 | /** 8 | * Initialize a new `Progress` indicator. 9 | */ 10 | 11 | function Progress() { 12 | this.percent = 0; 13 | this.size(0); 14 | this.fontSize(11); 15 | this.font('helvetica, arial, sans-serif'); 16 | } 17 | 18 | /** 19 | * Set progress size to `n`. 20 | * 21 | * @param {Number} n 22 | * @return {Progress} for chaining 23 | * @api public 24 | */ 25 | 26 | Progress.prototype.size = function(n){ 27 | this._size = n; 28 | return this; 29 | }; 30 | 31 | /** 32 | * Set text to `str`. 33 | * 34 | * @param {String} str 35 | * @return {Progress} for chaining 36 | * @api public 37 | */ 38 | 39 | Progress.prototype.text = function(str){ 40 | this._text = str; 41 | return this; 42 | }; 43 | 44 | /** 45 | * Set font size to `n`. 46 | * 47 | * @param {Number} n 48 | * @return {Progress} for chaining 49 | * @api public 50 | */ 51 | 52 | Progress.prototype.fontSize = function(n){ 53 | this._fontSize = n; 54 | return this; 55 | }; 56 | 57 | /** 58 | * Set font `family`. 59 | * 60 | * @param {String} family 61 | * @return {Progress} for chaining 62 | */ 63 | 64 | Progress.prototype.font = function(family){ 65 | this._font = family; 66 | return this; 67 | }; 68 | 69 | /** 70 | * Update percentage to `n`. 71 | * 72 | * @param {Number} n 73 | * @return {Progress} for chaining 74 | */ 75 | 76 | Progress.prototype.update = function(n){ 77 | this.percent = n; 78 | return this; 79 | }; 80 | 81 | /** 82 | * Draw on `ctx`. 83 | * 84 | * @param {CanvasRenderingContext2d} ctx 85 | * @return {Progress} for chaining 86 | */ 87 | 88 | Progress.prototype.draw = function(ctx){ 89 | try { 90 | var percent = Math.min(this.percent, 100) 91 | , size = this._size 92 | , half = size / 2 93 | , x = half 94 | , y = half 95 | , rad = half - 1 96 | , fontSize = this._fontSize; 97 | 98 | ctx.font = fontSize + 'px ' + this._font; 99 | 100 | var angle = Math.PI * 2 * (percent / 100); 101 | ctx.clearRect(0, 0, size, size); 102 | 103 | // outer circle 104 | ctx.strokeStyle = '#9f9f9f'; 105 | ctx.beginPath(); 106 | ctx.arc(x, y, rad, 0, angle, false); 107 | ctx.stroke(); 108 | 109 | // inner circle 110 | ctx.strokeStyle = '#eee'; 111 | ctx.beginPath(); 112 | ctx.arc(x, y, rad - 1, 0, angle, true); 113 | ctx.stroke(); 114 | 115 | // text 116 | var text = this._text || (percent | 0) + '%' 117 | , w = ctx.measureText(text).width; 118 | 119 | ctx.fillText( 120 | text 121 | , x - w / 2 + 1 122 | , y + fontSize / 2 - 1); 123 | } catch (ex) {} //don't fail if we can't render progress 124 | return this; 125 | }; 126 | -------------------------------------------------------------------------------- /lib/browser/tty.js: -------------------------------------------------------------------------------- 1 | exports.isatty = function(){ 2 | return true; 3 | }; 4 | 5 | exports.getWindowSize = function(){ 6 | if ('innerHeight' in global) { 7 | return [global.innerHeight, global.innerWidth]; 8 | } else { 9 | // In a Web Worker, the DOM Window is not available. 10 | return [640, 480]; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/formatter.js: -------------------------------------------------------------------------------- 1 | // A formatter is a Duplex stream that TAP data is written into, 2 | // and then something else (presumably not-TAP) is read from. 3 | // 4 | // See tap-classic.js for an example of a formatter in use. 5 | 6 | var Duplex = require('stream').Duplex 7 | var util = require('util') 8 | var Parser = require('tap-parser') 9 | util.inherits(Formatter, Duplex) 10 | module.exports = Formatter 11 | 12 | function Formatter(options, parser, parent) { 13 | if (!(this instanceof Formatter)) 14 | return new Formatter(options, parser, parent) 15 | 16 | if (!parser) 17 | parser = new Parser() 18 | 19 | Duplex.call(this, options) 20 | this.child = null 21 | this.parent = parent || null 22 | this.level = parser.level 23 | this.parser = parser 24 | 25 | attachEvents(this, parser, options) 26 | 27 | if (options.init) 28 | options.init.call(this) 29 | } 30 | 31 | function attachEvents (self, parser, options) { 32 | var events = [ 33 | 'version', 'plan', 'assert', 'comment', 34 | 'complete', 'extra', 'bailout' 35 | ] 36 | 37 | parser.on('child', function (childparser) { 38 | self.child = new Formatter(options, childparser, self) 39 | if (options.child) 40 | options.child.call(self, self.child) 41 | }) 42 | 43 | events.forEach(function (ev) { 44 | if (typeof options[ev] === 'function') 45 | parser.on(ev, options[ev].bind(self)) 46 | }) 47 | 48 | // proxy all stream events directly 49 | var streamEvents = [ 50 | 'pipe', 'prefinish', 'finish', 'unpipe', 'close' 51 | ] 52 | 53 | streamEvents.forEach(function (ev) { 54 | parser.on(ev, function () { 55 | var args = [ev] 56 | args.push.apply(args, arguments) 57 | self.emit.apply(self, args) 58 | }) 59 | }) 60 | } 61 | 62 | Formatter.prototype.write = function (c, e, cb) { 63 | return this.parser.write(c, e, cb) 64 | } 65 | 66 | Formatter.prototype.end = function (c, e, cb) { 67 | return this.parser.end(c, e, cb) 68 | } 69 | 70 | Formatter.prototype._read = function () {} 71 | 72 | // child formatters always push data to the root obj 73 | Formatter.prototype.push = function (c) { 74 | if (this.parent) 75 | return this.parent.push(c) 76 | 77 | Duplex.prototype.push.call(this, c) 78 | } 79 | -------------------------------------------------------------------------------- /lib/ms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helpers. 3 | */ 4 | 5 | var s = 1000; 6 | var m = s * 60; 7 | var h = m * 60; 8 | var d = h * 24; 9 | var y = d * 365.25; 10 | 11 | /** 12 | * Parse or format the given `val`. 13 | * 14 | * Options: 15 | * 16 | * - `long` verbose formatting [false] 17 | * 18 | * @param {String|Number} val 19 | * @param {Object} options 20 | * @return {String|Number} 21 | * @api public 22 | */ 23 | 24 | module.exports = function(val, options){ 25 | options = options || {}; 26 | if ('string' == typeof val) return parse(val); 27 | return options['long'] ? longFormat(val) : shortFormat(val); 28 | }; 29 | 30 | /** 31 | * Parse the given `str` and return milliseconds. 32 | * 33 | * @param {String} str 34 | * @return {Number} 35 | * @api private 36 | */ 37 | 38 | function parse(str) { 39 | var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); 40 | if (!match) return; 41 | var n = parseFloat(match[1]); 42 | var type = (match[2] || 'ms').toLowerCase(); 43 | switch (type) { 44 | case 'years': 45 | case 'year': 46 | case 'y': 47 | return n * y; 48 | case 'days': 49 | case 'day': 50 | case 'd': 51 | return n * d; 52 | case 'hours': 53 | case 'hour': 54 | case 'h': 55 | return n * h; 56 | case 'minutes': 57 | case 'minute': 58 | case 'm': 59 | return n * m; 60 | case 'seconds': 61 | case 'second': 62 | case 's': 63 | return n * s; 64 | case 'ms': 65 | return n; 66 | } 67 | } 68 | 69 | /** 70 | * Short format for `ms`. 71 | * 72 | * @param {Number} ms 73 | * @return {String} 74 | * @api private 75 | */ 76 | 77 | function shortFormat(ms) { 78 | if (ms >= d) return Math.round(ms / d) + 'd'; 79 | if (ms >= h) return Math.round(ms / h) + 'h'; 80 | if (ms >= m) return Math.round(ms / m) + 'm'; 81 | if (ms >= s) return Math.round(ms / s) + 's'; 82 | return ms + 'ms'; 83 | } 84 | 85 | /** 86 | * Long format for `ms`. 87 | * 88 | * @param {Number} ms 89 | * @return {String} 90 | * @api private 91 | */ 92 | 93 | function longFormat(ms) { 94 | return plural(ms, d, 'day') 95 | || plural(ms, h, 'hour') 96 | || plural(ms, m, 'minute') 97 | || plural(ms, s, 'second') 98 | || ms + ' ms'; 99 | } 100 | 101 | /** 102 | * Pluralization helper. 103 | */ 104 | 105 | function plural(ms, n, name) { 106 | if (ms < n) return; 107 | if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; 108 | return Math.ceil(ms / n) + ' ' + name + 's'; 109 | } 110 | -------------------------------------------------------------------------------- /lib/reporters/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var tty = require('tty') 6 | , diff = require('diff') 7 | , ms = require('../ms') 8 | , utils = require('../utils') 9 | , supportsColor = require('color-support')() 10 | 11 | /** 12 | * Save timer references to avoid Sinon interfering (see GH-237). 13 | */ 14 | 15 | var Date = global.Date 16 | , setTimeout = global.setTimeout 17 | , setInterval = global.setInterval 18 | , clearTimeout = global.clearTimeout 19 | , clearInterval = global.clearInterval; 20 | 21 | /** 22 | * Check if both stdio streams are associated with a tty. 23 | */ 24 | 25 | var isatty = tty.isatty(1); 26 | 27 | /** 28 | * Expose `Base`. 29 | */ 30 | 31 | exports = module.exports = Base; 32 | 33 | /** 34 | * Enable coloring by default, except in the browser interface. 35 | */ 36 | 37 | exports.useColors = process.env 38 | ? (supportsColor || (process.env.TAP_COLORS !== undefined)) 39 | : false; 40 | 41 | if (exports.useColors && +process.env.TAP_COLORS === 0) 42 | exports.useColors = false 43 | 44 | /** 45 | * Inline diffs instead of +/- 46 | */ 47 | 48 | exports.inlineDiffs = false; 49 | 50 | /** 51 | * Default color map. 52 | */ 53 | 54 | exports.colors = { 55 | 'pass': 90 56 | , 'fail': 31 57 | , 'bright pass': 92 58 | , 'bright fail': 91 59 | , 'bright yellow': 93 60 | , 'pending': 35 61 | , 'skip': 36 62 | , 'suite': 0 63 | , 'error title': 0 64 | , 'error message': 31 65 | , 'error stack': 90 66 | , 'checkmark': 32 67 | , 'fast': 90 68 | , 'medium': 33 69 | , 'slow': 31 70 | , 'green': 32 71 | , 'light': 90 72 | , 'diff gutter': 90 73 | , 'diff added': 42 74 | , 'diff removed': 41 75 | }; 76 | 77 | /** 78 | * Default symbol map. 79 | */ 80 | 81 | exports.symbols = { 82 | ok: '✓', 83 | err: '✖', 84 | dot: '․' 85 | }; 86 | 87 | // With node.js on Windows: use symbols available in terminal default fonts 88 | if ('win32' == process.platform) { 89 | exports.symbols.ok = '\u221A'; 90 | exports.symbols.err = '\u00D7'; 91 | exports.symbols.dot = '.'; 92 | } 93 | 94 | /** 95 | * Color `str` with the given `type`, 96 | * allowing colors to be disabled, 97 | * as well as user-defined color 98 | * schemes. 99 | * 100 | * @param {String} type 101 | * @param {String} str 102 | * @return {String} 103 | * @api private 104 | */ 105 | 106 | var color = exports.color = function(type, str) { 107 | if (!exports.useColors) return String(str); 108 | return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; 109 | }; 110 | 111 | /** 112 | * Expose term window size, with some 113 | * defaults for when stderr is not a tty. 114 | */ 115 | 116 | exports.window = { 117 | width: isatty 118 | ? process.stdout.getWindowSize 119 | ? process.stdout.getWindowSize(1)[0] 120 | : tty.getWindowSize()[1] 121 | : 75 122 | }; 123 | 124 | /** 125 | * Expose some basic cursor interactions 126 | * that are common among reporters. 127 | */ 128 | 129 | exports.cursor = { 130 | hide: function(){ 131 | isatty && process.stdout.write('\u001b[?25l'); 132 | }, 133 | 134 | show: function(){ 135 | isatty && process.stdout.write('\u001b[?25h'); 136 | }, 137 | 138 | deleteLine: function(){ 139 | isatty && process.stdout.write('\u001b[2K'); 140 | }, 141 | 142 | beginningOfLine: function(){ 143 | isatty && process.stdout.write('\u001b[0G'); 144 | }, 145 | 146 | CR: function(){ 147 | if (isatty) { 148 | exports.cursor.deleteLine(); 149 | exports.cursor.beginningOfLine(); 150 | } else { 151 | process.stdout.write('\r'); 152 | } 153 | } 154 | }; 155 | 156 | /** 157 | * Outut the given `failures` as a list. 158 | * 159 | * @param {Array} failures 160 | * @api public 161 | */ 162 | 163 | exports.list = function(failures){ 164 | console.log(); 165 | failures.forEach(function(test, i){ 166 | // format 167 | var fmt = color('error title', ' %s) %s:\n') 168 | + color('error message', ' %s') 169 | + color('error stack', '\n%s\n'); 170 | 171 | // msg 172 | var err = test.err 173 | , message = err.message || '' 174 | , stack = err.stack || message 175 | 176 | var index = stack.indexOf(message) + message.length 177 | , msg = stack.slice(0, index) 178 | , actual = err.actual 179 | , expected = err.expected 180 | , escape = true; 181 | 182 | 183 | // uncaught 184 | if (err.uncaught) { 185 | msg = 'Uncaught ' + msg; 186 | } 187 | // explicitly show diff 188 | if (err.showDiff && sameType(actual, expected)) { 189 | 190 | if ('string' !== typeof actual) { 191 | escape = false; 192 | err.actual = actual = utils.stringify(actual); 193 | err.expected = expected = utils.stringify(expected); 194 | } 195 | 196 | fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); 197 | var match = message.match(/^([^:]+): expected/); 198 | msg = '\n ' + color('error message', match ? match[1] : msg); 199 | 200 | if (exports.inlineDiffs) { 201 | msg += inlineDiff(err, escape); 202 | } else { 203 | msg += unifiedDiff(err, escape); 204 | } 205 | } 206 | 207 | // indent stack trace without msg 208 | stack = utils.stackTraceFilter()(stack.slice(index ? index + 1 : index) 209 | .replace(/^/gm, ' ')); 210 | 211 | console.log(fmt, (i + 1), test.fullTitle(), msg, stack); 212 | }); 213 | }; 214 | 215 | /** 216 | * Initialize a new `Base` reporter. 217 | * 218 | * All other reporters generally 219 | * inherit from this reporter, providing 220 | * stats such as test duration, number 221 | * of tests passed / failed etc. 222 | * 223 | * @param {Runner} runner 224 | * @api public 225 | */ 226 | 227 | function Base(runner) { 228 | var self = this 229 | , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } 230 | , failures = this.failures = []; 231 | 232 | if (!runner) return; 233 | this.runner = runner; 234 | 235 | runner.stats = stats; 236 | 237 | runner.on('start', function(){ 238 | stats.start = new Date; 239 | }); 240 | 241 | runner.on('suite', function(suite){ 242 | stats.suites = stats.suites || 0; 243 | suite.root || stats.suites++; 244 | }); 245 | 246 | runner.on('test end', function(test){ 247 | stats.tests = stats.tests || 0; 248 | stats.tests++; 249 | }); 250 | 251 | runner.on('pass', function(test){ 252 | stats.passes = stats.passes || 0; 253 | 254 | var medium = test.slow() / 2; 255 | test.speed = test.duration > test.slow() 256 | ? 'slow' 257 | : test.duration > medium 258 | ? 'medium' 259 | : 'fast'; 260 | 261 | stats.passes++; 262 | }); 263 | 264 | runner.on('fail', function(test, err){ 265 | stats.failures = stats.failures || 0; 266 | stats.failures++; 267 | test.err = err; 268 | failures.push(test); 269 | }); 270 | 271 | runner.on('end', function(){ 272 | stats.end = new Date; 273 | if (!stats.duration) 274 | stats.duration = stats.end - stats.start; 275 | }); 276 | 277 | runner.on('pending', function(){ 278 | stats.pending++; 279 | }); 280 | } 281 | 282 | /** 283 | * Output common epilogue used by many of 284 | * the bundled reporters. 285 | * 286 | * @api public 287 | */ 288 | 289 | Base.prototype.epilogue = function(){ 290 | var stats = this.stats; 291 | var tests; 292 | var fmt; 293 | 294 | console.log(); 295 | 296 | // passes 297 | fmt = color('bright pass', ' ') 298 | + color('green', ' %d passing') 299 | + color('light', ' (%s)'); 300 | 301 | console.log(fmt, 302 | stats.passes || 0, 303 | ms(stats.duration)); 304 | 305 | // pending 306 | if (stats.pending) { 307 | fmt = color('pending', ' ') 308 | + color('pending', ' %d pending'); 309 | 310 | console.log(fmt, stats.pending); 311 | } 312 | 313 | // failures 314 | if (stats.failures) { 315 | fmt = color('fail', ' %d failing'); 316 | 317 | console.log(fmt, stats.failures); 318 | 319 | Base.list(this.failures); 320 | } 321 | }; 322 | 323 | /** 324 | * Pad the given `str` to `len`. 325 | * 326 | * @param {String} str 327 | * @param {String} len 328 | * @return {String} 329 | * @api private 330 | */ 331 | 332 | function pad(str, len) { 333 | str = String(str); 334 | return Array(len - str.length + 1).join(' ') + str; 335 | } 336 | 337 | 338 | /** 339 | * Returns an inline diff between 2 strings with coloured ANSI output 340 | * 341 | * @param {Error} Error with actual/expected 342 | * @return {String} Diff 343 | * @api private 344 | */ 345 | 346 | function inlineDiff(err, escape) { 347 | var msg = errorDiff(err, 'WordsWithSpace', escape); 348 | 349 | // linenos 350 | var lines = msg.split('\n'); 351 | if (lines.length > 4) { 352 | var width = String(lines.length).length; 353 | msg = lines.map(function(str, i){ 354 | return pad(++i, width) + ' |' + ' ' + str; 355 | }).join('\n'); 356 | } 357 | 358 | // legend 359 | msg = '\n' 360 | + color('diff removed', 'actual') 361 | + ' ' 362 | + color('diff added', 'expected') 363 | + '\n\n' 364 | + msg 365 | + '\n'; 366 | 367 | // indent 368 | msg = msg.replace(/^/gm, ' '); 369 | return msg; 370 | } 371 | 372 | /** 373 | * Returns a unified diff between 2 strings 374 | * 375 | * @param {Error} Error with actual/expected 376 | * @return {String} Diff 377 | * @api private 378 | */ 379 | 380 | function unifiedDiff(err, escape) { 381 | var indent = ' '; 382 | function cleanUp(line) { 383 | if (escape) { 384 | line = escapeInvisibles(line); 385 | } 386 | if (line[0] === '+') return indent + colorLines('diff added', line); 387 | if (line[0] === '-') return indent + colorLines('diff removed', line); 388 | if (line.match(/\@\@/)) return null; 389 | if (line.match(/\\ No newline/)) return null; 390 | else return indent + line; 391 | } 392 | function notBlank(line) { 393 | return line != null; 394 | } 395 | 396 | var lines = (err.diff ? err.diff.split('\n').slice(2) : 397 | diff.createPatch('string', err.actual, err.expected) 398 | .split('\n').slice(4)); 399 | return '\n ' 400 | + colorLines('diff added', '+ expected') + ' ' 401 | + colorLines('diff removed', '- actual') 402 | + '\n\n' 403 | + lines.map(cleanUp).filter(notBlank).join('\n'); 404 | } 405 | 406 | /** 407 | * Return a character diff for `err`. 408 | * 409 | * @param {Error} err 410 | * @return {String} 411 | * @api private 412 | */ 413 | 414 | function errorDiff(err, type, escape) { 415 | var actual = escape ? escapeInvisibles(err.actual) : err.actual; 416 | var expected = escape ? escapeInvisibles(err.expected) : err.expected; 417 | return diff['diff' + type](actual, expected).map(function(str){ 418 | if (str.added) return colorLines('diff added', str.value); 419 | if (str.removed) return colorLines('diff removed', str.value); 420 | return str.value; 421 | }).join(''); 422 | } 423 | 424 | /** 425 | * Returns a string with all invisible characters in plain text 426 | * 427 | * @param {String} line 428 | * @return {String} 429 | * @api private 430 | */ 431 | function escapeInvisibles(line) { 432 | return line.replace(/\t/g, '') 433 | .replace(/\r/g, '') 434 | .replace(/\n/g, '\n'); 435 | } 436 | 437 | /** 438 | * Color lines for `str`, using the color `name`. 439 | * 440 | * @param {String} name 441 | * @param {String} str 442 | * @return {String} 443 | * @api private 444 | */ 445 | 446 | function colorLines(name, str) { 447 | return str.split('\n').map(function(str){ 448 | return color(name, str); 449 | }).join('\n'); 450 | } 451 | 452 | /** 453 | * Check that a / b have the same type. 454 | * 455 | * @param {Object} a 456 | * @param {Object} b 457 | * @return {Boolean} 458 | * @api private 459 | */ 460 | 461 | function sameType(a, b) { 462 | a = Object.prototype.toString.call(a); 463 | b = Object.prototype.toString.call(b); 464 | return a == b; 465 | } 466 | -------------------------------------------------------------------------------- /lib/reporters/classic.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Classic 2 | 3 | var Base = require('./base') 4 | , cursor = Base.cursor 5 | , color = Base.color 6 | , yaml = require('tap-yaml') 7 | , util = require('util') 8 | , fancy = Base.useColors && !process.env.TRAVIS 9 | , ms = require('../ms.js') 10 | , diff = require('diff') 11 | , utils = require('../utils.js') 12 | , uclen = require('unicode-length').get 13 | , colorSupport = require('color-support')() 14 | 15 | function repeat (n, c) { 16 | return new Array(Math.max(n + 1, 0)).join(c) 17 | } 18 | 19 | function hasOwnProperty (obj, key) { 20 | return Object.prototype.hasOwnProperty.call(obj, key) 21 | } 22 | 23 | function doDiff (found, wanted, patch) { 24 | // TODO: Make this a configurable thing or something? 25 | // 26 | // Choosing a palette for diffs in a test-runner context 27 | // is really tricky. The temptation is to make it look 28 | // exactly like `git diff`, but the default red and green 29 | // are very confusing with the colors used to indicate 30 | // pass/fail. 31 | // 32 | // So, I decided to experiment with setting a background to 33 | // distinguish the diff section from the rest of the test 34 | // output. The obvious choice, then, was to mimick GitHub's 35 | // diff styling. 36 | // 37 | // The problem there is that, while those of us with an 38 | // abundance of cones tend to find that palette most pleasing, 39 | // it's virtually impossible for people with various sorts of 40 | // red/green colorblindness to distinguish those colors. 41 | // 42 | // The resulting option, with a somewhat pink-ish red and a 43 | // somewhat yellow-ish green, seems to be a pretty good 44 | // compromise between esthetics and accessibility. In a poll 45 | // on twitter, it was the only one that no color-sighted people 46 | // strongly objected to, and no color-blind people had 47 | // significant trouble interpreting. The twitter poll agrees 48 | // with the results of Sim Daltonism, which showed that this 49 | // palette was easily distinguishable across all forms of color 50 | // deficiency. 51 | 52 | // TODO: add a TrueColor one that looks nicer 53 | var palette = colorSupport.has256 ? 6 : 1 54 | 55 | var plain = '' 56 | var reset = '\u001b[m' 57 | 58 | switch (palette) { 59 | case 1: 60 | // most git-like. r/g, no background, no bold 61 | var bg = '' 62 | var removed = '\u001b[31m' 63 | var added = '\u001b[32m' 64 | break 65 | 66 | case 2: 67 | // dark option, maybe confusing with pass/fail colors? 68 | var bg = '\u001b[48;5;234m' 69 | var removed = '\u001b[31;1m' 70 | var added = '\u001b[32;1m' 71 | break 72 | 73 | case 3: 74 | // pastel option, most githubby 75 | var removed = '\u001b[48;5;224m\u001b[38;5;52m' 76 | var added = '\u001b[48;5;194m\u001b[38;5;22m' 77 | var plain = '\u001b[38;5;233m' 78 | var bg = '\u001b[48;5;255m' 79 | break 80 | 81 | case 4: 82 | // orange/cyan pastel option, most r/g-colorblind friendly 83 | var removed = '\u001b[48;5;223m\u001b[38;5;52m' 84 | var added = '\u001b[48;5;158m\u001b[38;5;22m' 85 | var plain = '\u001b[38;5;233m' 86 | var bg = '\u001b[48;5;255m' 87 | break 88 | 89 | case 5: 90 | // pastel option, green is bluish, red is just red 91 | var removed = '\u001b[48;5;224m\u001b[38;5;52m' 92 | var added = '\u001b[48;5;158m\u001b[38;5;22m' 93 | var plain = '\u001b[38;5;233m' 94 | var bg = '\u001b[48;5;255m' 95 | break 96 | 97 | case 6: 98 | // pastel option, red is purplish, green is yellowish 99 | var removed = '\u001b[48;5;218m\u001b[38;5;52m' 100 | var added = '\u001b[48;5;193m\u001b[38;5;22m' 101 | var plain = '\u001b[38;5;233m' 102 | var bg = '\u001b[48;5;255m' 103 | break 104 | 105 | case 7: 106 | // pastel option, red is purplish, green is just green 107 | var removed = '\u001b[48;5;218m\u001b[38;5;52m' 108 | var added = '\u001b[48;5;194m\u001b[38;5;22m' 109 | var plain = '\u001b[38;5;233m' 110 | var bg = '\u001b[48;5;255m' 111 | break 112 | 113 | case 8: 114 | // pastel, red and blue 115 | var removed = '\u001b[48;5;224m\u001b[38;5;52m' 116 | var added = '\u001b[48;5;189m\u001b[38;5;17m' 117 | var plain = '\u001b[38;5;233m' 118 | var bg = '\u001b[48;5;255m' 119 | break 120 | 121 | case 9: 122 | // pastel option, red is purplish, green is yellowish 123 | var removed = '\u001b[48;5;224m\u001b[38;5;52m' 124 | var added = '\u001b[48;5;193m\u001b[38;5;22m' 125 | var plain = '\u001b[38;5;233m' 126 | var bg = '\u001b[48;5;255m' 127 | break 128 | 129 | } 130 | 131 | 132 | var maxLen = process.stdout.columns || 0 133 | if (maxLen >= 5) 134 | maxLen -= 5 135 | 136 | if (!Base.useColors) { 137 | bg = removed = added = reset = plain = '' 138 | maxLen = 0 139 | } 140 | 141 | // If they are not strings, or only differ in trailing whitespace, 142 | // then stringify them so that we can see the difference. 143 | if (typeof found !== 'string' || 144 | typeof wanted !== 'string' || 145 | found.trim() === wanted.trim()) { 146 | found = utils.stringify(found) 147 | wanted = utils.stringify(wanted) 148 | } 149 | 150 | var width = 0 151 | patch = patch || ( 152 | ['--- wanted', '+++ found'].concat( 153 | diff.createPatch('', wanted, found).split(/\n/).slice(5) 154 | ).join('\n')) 155 | 156 | patch = patch.split('\n').map(function (line, index) { 157 | if (uclen(line) > width) 158 | width = Math.min(maxLen, uclen(line)) 159 | if (line.match(/^\=+$/) || 160 | line === '\\ No newline at end of file') 161 | return null 162 | else 163 | return line 164 | }).filter(function (line, i) { 165 | return line 166 | }).map(function (line) { 167 | if (uclen(line) <= width) 168 | line += repeat(width - uclen(line) + 1, ' ') 169 | return line 170 | }).map(function (line) { 171 | if (line.charAt(0) === '+') 172 | return bg + added + line + reset 173 | else if (line.charAt(0) === '-') 174 | return bg + removed + line + reset 175 | else 176 | return bg + plain + line + reset 177 | }).join('\n') 178 | 179 | var pref ='' 180 | 181 | return pref + patch 182 | } 183 | 184 | util.inherits(Classic, Base) 185 | 186 | function Classic (runner) { 187 | Base.call(this, runner); 188 | 189 | var self = this 190 | 191 | var grandTotal = 0 192 | var grandPass = 0 193 | 194 | var bailed = false 195 | var hadFails = false 196 | var currentSuite = null 197 | var tests = [] 198 | var skipped = 0 199 | var skipMsg = [] 200 | var todo = [] 201 | var fails = [] 202 | var total = 0 203 | var pass = 0 204 | var tickDots = 0 205 | var tickColor = 'checkmark' 206 | 207 | runner.on('bailout', function (bailout, suite) { 208 | if (currentSuite) 209 | runner.emit('suite end', currentSuite) 210 | if (bailed) 211 | return 212 | bailed = true 213 | console.log(Base.color('fail', 'Bail out! ' + bailout)) 214 | }) 215 | 216 | runner.on('suite', function (suite) { 217 | if (!suite.root) 218 | return 219 | 220 | if (fancy) { 221 | process.stdout.write(suite.title + ' ') 222 | tickDots = 0 223 | tickColor = 'checkmark' 224 | } 225 | 226 | currentSuite = suite 227 | tests = [] 228 | todo = [] 229 | fails = [] 230 | skipMsg = [] 231 | skipped = 0 232 | pass = 0 233 | total = 0 234 | }) 235 | 236 | runner.on('suite end', function (suite) { 237 | if (!suite.root) 238 | return 239 | 240 | if (fancy) 241 | Base.cursor.beginningOfLine() 242 | 243 | currentSuite = null 244 | var len = 60 245 | var title = suite.title || '(unnamed)' 246 | var num = pass + '/' + total 247 | var dots = len - uclen(title) - uclen(num) - 3 248 | if (dots < 2) 249 | dots = 2 250 | dots = ' ' + repeat(dots, '.') + ' ' 251 | if (fails.length) 252 | num = Base.color('fail', num) 253 | else if (pass === total) 254 | num = Base.color('checkmark', num) 255 | else 256 | num = Base.color('pending', num) 257 | 258 | var fmt = title + dots + num 259 | if (suite.duration / total > 250) 260 | fmt += Base.color('slow', ' ' + ms(Math.round(suite.duration))) 261 | 262 | console.log(fmt) 263 | 264 | if (fails.length) { 265 | var failMsg = '' 266 | fails.forEach(function (t) { 267 | if (t.parent) 268 | failMsg += t.parent + '\n' 269 | failMsg += Base.color('fail', 'not ok ' + t.name) + '\n' 270 | if (t.diag) { 271 | var printDiff = false 272 | if (hasOwnProperty(t.diag, 'found') && 273 | hasOwnProperty(t.diag, 'wanted') || 274 | hasOwnProperty(t.diag, 'diff')) { 275 | printDiff = true 276 | var found = t.diag.found 277 | var wanted = t.diag.wanted 278 | var diff = doDiff(found, wanted, t.diag.diff) 279 | failMsg += indent(diff, 2) + '\n' 280 | } 281 | 282 | var o = {} 283 | var print = false 284 | for (var i in t.diag) { 285 | // Don't re-print what we already showed in the diff 286 | if (printDiff && ( i === 'found' || i === 'wanted' || i === 'pattern' || i === 'diff')) 287 | continue 288 | o[i] = t.diag[i] 289 | print = true 290 | } 291 | if (print) 292 | failMsg += indent(yaml.stringify(o), 2) + '\n' 293 | } 294 | }) 295 | console.log(indent(failMsg, 2)) 296 | } 297 | 298 | if (todo.length) { 299 | var todoMsg = '' 300 | var bullet = Base.color('pending', '~ ') 301 | todo.forEach(function (t) { 302 | if (t.todo !== true) 303 | t.name += ' - ' + Base.color('pending', t.todo) 304 | todoMsg += bullet + t.name + '\n' 305 | if (t.diag) 306 | todoMsg += indent(yaml.stringify(t.diag), 4) + '\n' 307 | }) 308 | console.log(indent(todoMsg, 2)) 309 | } 310 | 311 | if (skipped) { 312 | var fmt = Base.color('skip', indent('Skipped: %d', 2)) 313 | console.log(fmt, skipped) 314 | if (skipMsg.length) 315 | console.log(indent(skipMsg.join('\n'), 4)) 316 | console.log('') 317 | } 318 | }) 319 | 320 | runner.on('test', function (test) { 321 | total ++ 322 | grandTotal ++ 323 | var t = test.result 324 | if (fancy && currentSuite) { 325 | var max = 57 - uclen(currentSuite.title) 326 | if (max < 3) 327 | max = 3 328 | 329 | if (tickDots > max) { 330 | tickDots = 0 331 | Base.cursor.deleteLine() 332 | Base.cursor.beginningOfLine(); 333 | process.stdout.write(currentSuite.title + ' ') 334 | } 335 | tickDots ++ 336 | 337 | if (t.todo && 338 | (tickColor === 'checkmark' || tickColor === 'skip')) 339 | tickColor = 'pending' 340 | else if (t.skip && tickColor === 'checkmark') 341 | tickColor = 'skip' 342 | else if (!t.ok) 343 | tickColor = 'fail' 344 | 345 | process.stdout.write(Base.color(tickColor, '.')) 346 | } 347 | 348 | if (t.skip) { 349 | skipped += 1 350 | if (!/^filter(( out)?: \/.+\/|: only)$/.test(t.skip)) { 351 | if (t.skip !== true) 352 | skipMsg.push(t.name + ' ' + Base.color('skip', t.skip)) 353 | else 354 | skipMsg.push(t.name) 355 | } 356 | } 357 | else if (t.todo) 358 | todo.push(t) 359 | else if (!t.ok) { 360 | t.parent = [] 361 | var p = test.parent 362 | while (p && p !== currentSuite) { 363 | var n = p.title || p.name || p.fullTitle() 364 | if (n) 365 | t.parent.unshift(n) 366 | p = p.parent 367 | } 368 | t.parent.shift() 369 | t.parent = t.parent.join(' > ') 370 | fails.push(t) 371 | hadFails = true 372 | } 373 | else { 374 | pass ++ 375 | grandPass ++ 376 | } 377 | }) 378 | 379 | runner.on('end', function () { 380 | total = grandTotal 381 | pass = grandPass 382 | tests = [] 383 | todo = [] 384 | fails = [] 385 | skipMsg = [] 386 | skipped = 0 387 | if (hadFails) 388 | fails = [,,,] 389 | runner.emit('suite end', { title: 'total', root: true }) 390 | self.failures = [] 391 | self.epilogue(); 392 | 393 | if (grandTotal === grandPass) { 394 | console.log(Base.color('checkmark', '\n ok')) 395 | } 396 | }) 397 | } 398 | 399 | function indent (str, n) { 400 | var ind = repeat(n, ' ') 401 | str = ind + str.split('\n').join('\n' + ind) 402 | return str.replace(/(\n\s*)+$/, '\n') 403 | } 404 | -------------------------------------------------------------------------------- /lib/reporters/doc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , utils = require('../utils'); 7 | 8 | /** 9 | * Expose `Doc`. 10 | */ 11 | 12 | exports = module.exports = Doc; 13 | 14 | /** 15 | * Initialize a new `Doc` reporter. 16 | * 17 | * @param {Runner} runner 18 | * @api public 19 | */ 20 | 21 | function Doc(runner) { 22 | Base.call(this, runner); 23 | 24 | var self = this 25 | , stats = this.stats 26 | , total = runner.total 27 | , indents = 2; 28 | 29 | function indent() { 30 | return Array(indents).join(' '); 31 | } 32 | 33 | runner.on('suite', function(suite){ 34 | if (suite.root) return; 35 | ++indents; 36 | console.log('%s
', indent()); 37 | ++indents; 38 | console.log('%s

%s

', indent(), utils.escape(suite.title)); 39 | console.log('%s
', indent()); 40 | }); 41 | 42 | runner.on('suite end', function(suite){ 43 | if (suite.root) return; 44 | console.log('%s
', indent()); 45 | --indents; 46 | console.log('%s
', indent()); 47 | --indents; 48 | }); 49 | 50 | runner.on('pass', function(test){ 51 | console.log('%s
%s
', indent(), utils.escape(test.title)); 52 | var code = utils.escape(utils.clean(test.fn.toString())); 53 | console.log('%s
%s
', indent(), code); 54 | }); 55 | 56 | runner.on('fail', function(test, err){ 57 | console.log('%s
%s
', indent(), utils.escape(test.title)); 58 | var code = utils.escape(utils.clean(test.fn.toString())); 59 | console.log('%s
%s
', indent(), code); 60 | console.log('%s
%s
', indent(), utils.escape(err)); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /lib/reporters/dot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , color = Base.color; 7 | 8 | /** 9 | * Expose `Dot`. 10 | */ 11 | 12 | exports = module.exports = Dot; 13 | 14 | /** 15 | * Initialize a new `Dot` matrix test reporter. 16 | * 17 | * @param {Runner} runner 18 | * @api public 19 | */ 20 | 21 | function Dot(runner) { 22 | Base.call(this, runner); 23 | 24 | var self = this 25 | , stats = this.stats 26 | , width = Base.window.width * .75 | 0 27 | , n = -1; 28 | 29 | runner.on('start', function(){ 30 | process.stdout.write('\n'); 31 | }); 32 | 33 | runner.on('pending', function(test){ 34 | if (++n % width == 0) process.stdout.write('\n '); 35 | process.stdout.write(color('pending', Base.symbols.dot)); 36 | }); 37 | 38 | runner.on('pass', function(test){ 39 | if (++n % width == 0) process.stdout.write('\n '); 40 | if ('slow' == test.speed) { 41 | process.stdout.write(color('bright yellow', Base.symbols.dot)); 42 | } else { 43 | process.stdout.write(color(test.speed, Base.symbols.dot)); 44 | } 45 | }); 46 | 47 | runner.on('fail', function(test, err){ 48 | if (++n % width == 0) process.stdout.write('\n '); 49 | process.stdout.write(color('fail', Base.symbols.dot)); 50 | }); 51 | 52 | runner.on('end', function(){ 53 | console.log(); 54 | self.epilogue(); 55 | }); 56 | } 57 | 58 | /** 59 | * Inherit from `Base.prototype`. 60 | */ 61 | 62 | Object.setPrototypeOf(Dot.prototype, Base.prototype); 63 | -------------------------------------------------------------------------------- /lib/reporters/dump.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = Dump 2 | var Base = require('./base') 3 | , cursor = Base.cursor 4 | , color = Base.color 5 | , useColors = Base.useColors 6 | , util = require('util') 7 | 8 | function Dump(runner) { 9 | Base.call(this, runner); 10 | 11 | var events = [ 12 | 'start', 13 | 'version', 14 | 'suite', 15 | 'suite end', 16 | 'test', 17 | 'pending', 18 | 'pass', 19 | 'fail', 20 | 'test end', 21 | ]; 22 | 23 | var i = process.argv.indexOf('dump') 24 | if (i !== -1) { 25 | var args = process.argv.slice(i + 1) 26 | if (args.length) 27 | events = args 28 | } 29 | 30 | runner.on('line', function (c) { 31 | if (c.trim()) 32 | process.stderr.write(Base.color('bright yellow', c)) 33 | }) 34 | 35 | events.forEach(function (ev) { 36 | runner.on(ev, function (obj) { 37 | console.log(ev) 38 | if (arguments.length) { 39 | console.log(util.inspect(obj, false, Infinity, useColors)) 40 | console.log() 41 | } 42 | }) 43 | }) 44 | 45 | runner.on('end', function () { 46 | console.log('end') 47 | console.log(runner.stats) 48 | console.log() 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /lib/reporters/index.js: -------------------------------------------------------------------------------- 1 | exports.dot = require('./dot.js') 2 | exports.doc = require('./doc.js') 3 | exports.tap = true 4 | exports.json = require('./json.js') 5 | exports.list = require('./list.js') 6 | exports.min = require('./min.js') 7 | exports.spec = require('./spec.js') 8 | exports.nyan = require('./nyan.js') 9 | exports.xunit = require('./xunit.js') 10 | exports.markdown = require('./markdown.js') 11 | exports.progress = require('./progress.js') 12 | exports.landing = require('./landing.js') 13 | exports.jsonstream = require('./json-stream.js') 14 | exports.dump = require('./dump.js') 15 | exports.classic = require('./classic.js') 16 | exports.silent = require('./silent.js') 17 | -------------------------------------------------------------------------------- /lib/reporters/json-stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , color = Base.color; 7 | 8 | /** 9 | * Expose `List`. 10 | */ 11 | 12 | exports = module.exports = List; 13 | 14 | /** 15 | * Initialize a new `List` test reporter. 16 | * 17 | * @param {Runner} runner 18 | * @api public 19 | */ 20 | 21 | function List(runner) { 22 | Base.call(this, runner); 23 | 24 | var self = this 25 | , stats = this.stats 26 | , total = runner.total; 27 | 28 | runner.on('start', function(){ 29 | console.log(JSON.stringify(['start', { total: total }])); 30 | }); 31 | 32 | runner.on('pass', function(test){ 33 | console.log(JSON.stringify(['pass', clean(test)])); 34 | }); 35 | 36 | runner.on('fail', function(test, err){ 37 | test = clean(test); 38 | test.err = err.message; 39 | console.log(JSON.stringify(['fail', test])); 40 | }); 41 | 42 | runner.on('end', function(){ 43 | process.stdout.write(JSON.stringify(['end', self.stats])); 44 | }); 45 | } 46 | 47 | /** 48 | * Return a plain-object representation of `test` 49 | * free of cyclic properties etc. 50 | * 51 | * @param {Object} test 52 | * @return {Object} 53 | * @api private 54 | */ 55 | 56 | function clean(test) { 57 | return { 58 | title: test.title 59 | , fullTitle: test.fullTitle() 60 | , duration: test.duration 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/reporters/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , cursor = Base.cursor 7 | , color = Base.color; 8 | 9 | /** 10 | * Expose `JSON`. 11 | */ 12 | 13 | exports = module.exports = JSONReporter; 14 | 15 | /** 16 | * Initialize a new `JSON` reporter. 17 | * 18 | * @param {Runner} runner 19 | * @api public 20 | */ 21 | 22 | function JSONReporter(runner) { 23 | var self = this; 24 | Base.call(this, runner); 25 | 26 | var tests = [] 27 | , pending = [] 28 | , failures = [] 29 | , passes = []; 30 | 31 | runner.on('test end', function(test){ 32 | tests.push(test); 33 | }); 34 | 35 | runner.on('pass', function(test){ 36 | passes.push(test); 37 | }); 38 | 39 | runner.on('fail', function(test){ 40 | failures.push(test); 41 | }); 42 | 43 | runner.on('pending', function(test){ 44 | pending.push(test); 45 | }); 46 | 47 | runner.on('end', function(){ 48 | var obj = { 49 | stats: self.stats, 50 | tests: tests.map(clean), 51 | pending: pending.map(clean), 52 | failures: failures.map(clean), 53 | passes: passes.map(clean) 54 | }; 55 | 56 | runner.testResults = obj; 57 | 58 | process.stdout.write(JSON.stringify(obj, null, 2)); 59 | }); 60 | } 61 | 62 | /** 63 | * Return a plain-object representation of `test` 64 | * free of cyclic properties etc. 65 | * 66 | * @param {Object} test 67 | * @return {Object} 68 | * @api private 69 | */ 70 | 71 | function clean(test) { 72 | return { 73 | title: test.title, 74 | fullTitle: test.fullTitle(), 75 | duration: test.duration, 76 | err: errorJSON(test.err || {}) 77 | } 78 | } 79 | 80 | /** 81 | * Transform `error` into a JSON object. 82 | * @param {Error} err 83 | * @return {Object} 84 | */ 85 | 86 | function errorJSON(err) { 87 | var res = {}; 88 | Object.getOwnPropertyNames(err).forEach(function(key) { 89 | res[key] = err[key]; 90 | }, err); 91 | return res; 92 | } 93 | -------------------------------------------------------------------------------- /lib/reporters/landing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , cursor = Base.cursor 7 | , color = Base.color; 8 | 9 | /** 10 | * Expose `Landing`. 11 | */ 12 | 13 | exports = module.exports = Landing; 14 | 15 | /** 16 | * Airplane color. 17 | */ 18 | 19 | Base.colors.plane = 0; 20 | 21 | /** 22 | * Airplane crash color. 23 | */ 24 | 25 | Base.colors['plane crash'] = 31; 26 | 27 | /** 28 | * Runway color. 29 | */ 30 | 31 | Base.colors.runway = 90; 32 | 33 | /** 34 | * Initialize a new `Landing` reporter. 35 | * 36 | * @param {Runner} runner 37 | * @api public 38 | */ 39 | 40 | function Landing(runner) { 41 | Base.call(this, runner); 42 | 43 | var self = this 44 | , stats = this.stats 45 | , width = Base.window.width * .75 | 0 46 | , total = runner.total 47 | , stream = process.stdout 48 | , plane = color('plane', '✈') 49 | , crashed = -1 50 | , n = 0; 51 | 52 | function runway() { 53 | var buf = Array(width).join('-'); 54 | return ' ' + color('runway', buf); 55 | } 56 | 57 | runner.on('start', function(){ 58 | stream.write('\n\n\n '); 59 | cursor.hide(); 60 | }); 61 | 62 | runner.on('test end', function(test){ 63 | // check if the plane crashed 64 | var col = -1 == crashed 65 | ? width * ++n / total | 0 66 | : crashed; 67 | 68 | // show the crash 69 | if ('failed' == test.state) { 70 | plane = color('plane crash', '✈'); 71 | crashed = col; 72 | } 73 | 74 | // render landing strip 75 | stream.write('\u001b['+(width+1)+'D\u001b[2A'); 76 | stream.write(runway()); 77 | stream.write('\n '); 78 | stream.write(color('runway', Array(col).join('⋅'))); 79 | stream.write(plane) 80 | stream.write(color('runway', Array(width - col).join('⋅') + '\n')); 81 | stream.write(runway()); 82 | stream.write('\u001b[0m'); 83 | }); 84 | 85 | runner.on('end', function(){ 86 | cursor.show(); 87 | console.log(); 88 | self.epilogue(); 89 | }); 90 | } 91 | 92 | /** 93 | * Inherit from `Base.prototype`. 94 | */ 95 | 96 | Object.setPrototypeOf(Landing.prototype, Base.prototype); 97 | -------------------------------------------------------------------------------- /lib/reporters/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , cursor = Base.cursor 7 | , color = Base.color; 8 | 9 | /** 10 | * Expose `List`. 11 | */ 12 | 13 | exports = module.exports = List; 14 | 15 | /** 16 | * Initialize a new `List` test reporter. 17 | * 18 | * @param {Runner} runner 19 | * @api public 20 | */ 21 | 22 | function List(runner) { 23 | Base.call(this, runner); 24 | 25 | var self = this 26 | , stats = this.stats 27 | , n = 0; 28 | 29 | runner.on('start', function(){ 30 | console.log(); 31 | }); 32 | 33 | runner.on('test', function(test){ 34 | process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); 35 | }); 36 | 37 | runner.on('pending', function(test){ 38 | var fmt = color('checkmark', ' -') 39 | + color('pending', ' %s'); 40 | console.log(fmt, test.fullTitle()); 41 | }); 42 | 43 | runner.on('pass', function(test){ 44 | var fmt = color('checkmark', ' '+Base.symbols.dot) 45 | + color('pass', ' %s') 46 | + (test.duration ? color('pass', ': ') + color(test.speed, '%dms') : ''); 47 | cursor.CR(); 48 | console.log(fmt, test.fullTitle(), test.duration || ''); 49 | }); 50 | 51 | runner.on('fail', function(test, err){ 52 | cursor.CR(); 53 | console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); 54 | }); 55 | 56 | runner.on('end', self.epilogue.bind(self)); 57 | } 58 | 59 | /** 60 | * Inherit from `Base.prototype`. 61 | */ 62 | 63 | Object.setPrototypeOf(List.prototype, Base.prototype); 64 | -------------------------------------------------------------------------------- /lib/reporters/markdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , utils = require('../utils'); 7 | 8 | /** 9 | * Constants 10 | */ 11 | 12 | var SUITE_PREFIX = '$'; 13 | 14 | /** 15 | * Expose `Markdown`. 16 | */ 17 | 18 | exports = module.exports = Markdown; 19 | 20 | /** 21 | * Initialize a new `Markdown` reporter. 22 | * 23 | * @param {Runner} runner 24 | * @api public 25 | */ 26 | 27 | function Markdown(runner) { 28 | Base.call(this, runner); 29 | 30 | var self = this 31 | , stats = this.stats 32 | , level = 0 33 | , buf = ''; 34 | 35 | function title(str) { 36 | return Array(level + 1).join('#') + ' ' + str; 37 | } 38 | 39 | function indent() { 40 | return Array(level).join(' '); 41 | } 42 | 43 | function mapTOC(suite, obj) { 44 | var ret = obj, 45 | key = SUITE_PREFIX + suite.title; 46 | obj = obj[key] = obj[key] || { suite: suite }; 47 | suite.suites.forEach(function(suite){ 48 | mapTOC(suite, obj); 49 | }); 50 | return ret; 51 | } 52 | 53 | function stringifyTOC(obj, level) { 54 | ++level; 55 | var buf = ''; 56 | var link; 57 | for (var key in obj) { 58 | if ('suite' == key) continue; 59 | if (key !== SUITE_PREFIX) { 60 | link = ' - [' + key.substring(1) + ']'; 61 | link += '(#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; 62 | buf += Array(level).join(' ') + link; 63 | } 64 | buf += stringifyTOC(obj[key], level); 65 | } 66 | return buf; 67 | } 68 | 69 | function generateTOC() { 70 | return suites.map(generateTOC_).join('') 71 | } 72 | 73 | function generateTOC_(suite) { 74 | var obj = mapTOC(suite, {}); 75 | return stringifyTOC(obj, 0); 76 | } 77 | 78 | var suites = [] 79 | var currentSuite = null 80 | runner.on('suite', function(suite){ 81 | currentSuite = suite 82 | if (suite.root) { 83 | suites.push(suite) 84 | } 85 | ++level; 86 | var slug = utils.slug(suite.fullTitle()); 87 | buf += '' + '\n'; 88 | buf += title(suite.title) + '\n'; 89 | }); 90 | 91 | runner.on('suite end', function(suite){ 92 | if (suite.ok) { 93 | buf += '\nok - ' + suite.title + '\n' 94 | } else { 95 | buf += '\nnot ok - ' + suite.title + '\n' 96 | } 97 | --level; 98 | }); 99 | 100 | runner.on('test', function(test){ 101 | if (!test.ok || test.pending) { 102 | var code = utils.clean(test.fn.toString()); 103 | buf += test.title + '.\n'; 104 | if (code) { 105 | buf += '\n```js\n'; 106 | buf += code + '\n'; 107 | buf += '```\n'; 108 | } 109 | var stack = test.err && test.err.stack 110 | if (!stack) { 111 | stack = test.result && test.result.diag && test.result.diag.stack 112 | } 113 | if (stack) { 114 | buf += '\n```\n' + stack + '\n```\n'; 115 | } 116 | buf += '\n\n'; 117 | } 118 | }); 119 | 120 | runner.on('end', function(){ 121 | process.stdout.write('# TOC\n'); 122 | process.stdout.write(generateTOC()); 123 | process.stdout.write('\n\n'); 124 | process.stdout.write(buf); 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /lib/reporters/min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base'); 6 | 7 | /** 8 | * Expose `Min`. 9 | */ 10 | 11 | exports = module.exports = Min; 12 | 13 | /** 14 | * Initialize a new `Min` minimal test reporter (best used with --watch). 15 | * 16 | * @param {Runner} runner 17 | * @api public 18 | */ 19 | 20 | function Min(runner) { 21 | Base.call(this, runner); 22 | 23 | runner.on('start', function(){ 24 | // clear screen 25 | process.stdout.write('\u001b[2J'); 26 | // set cursor position 27 | process.stdout.write('\u001b[1;3H'); 28 | }); 29 | 30 | runner.on('end', this.epilogue.bind(this)); 31 | } 32 | 33 | /** 34 | * Inherit from `Base.prototype`. 35 | */ 36 | 37 | Object.setPrototypeOf(Min.prototype, Base.prototype); 38 | -------------------------------------------------------------------------------- /lib/reporters/nyan.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base'); 6 | 7 | /** 8 | * Expose `Dot`. 9 | */ 10 | 11 | exports = module.exports = NyanCat; 12 | 13 | /** 14 | * Initialize a new `Dot` matrix test reporter. 15 | * 16 | * @param {Runner} runner 17 | * @api public 18 | */ 19 | 20 | function NyanCat(runner) { 21 | Base.call(this, runner); 22 | var self = this 23 | , stats = this.stats 24 | , width = Base.window.width * .75 | 0 25 | , rainbowColors = this.rainbowColors = self.generateColors() 26 | , colorIndex = this.colorIndex = 0 27 | , numerOfLines = this.numberOfLines = 4 28 | , trajectories = this.trajectories = [[], [], [], []] 29 | , nyanCatWidth = this.nyanCatWidth = 11 30 | , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth) 31 | , scoreboardWidth = this.scoreboardWidth = 5 32 | , tick = this.tick = 0 33 | , n = 0; 34 | 35 | runner.on('start', function(){ 36 | Base.cursor.hide(); 37 | self.draw(); 38 | }); 39 | 40 | runner.on('pending', function(test){ 41 | self.draw(); 42 | }); 43 | 44 | runner.on('pass', function(test){ 45 | self.draw(); 46 | }); 47 | 48 | runner.on('fail', function(test, err){ 49 | self.draw(); 50 | }); 51 | 52 | runner.on('end', function(){ 53 | Base.cursor.show(); 54 | for (var i = 0; i < self.numberOfLines; i++) write('\n'); 55 | self.epilogue(); 56 | }); 57 | } 58 | 59 | /** 60 | * Draw the nyan cat 61 | * 62 | * @api private 63 | */ 64 | 65 | NyanCat.prototype.draw = function(){ 66 | this.appendRainbow(); 67 | this.drawScoreboard(); 68 | this.drawRainbow(); 69 | this.drawNyanCat(); 70 | this.tick = !this.tick; 71 | }; 72 | 73 | /** 74 | * Draw the "scoreboard" showing the number 75 | * of passes, failures and pending tests. 76 | * 77 | * @api private 78 | */ 79 | 80 | NyanCat.prototype.drawScoreboard = function(){ 81 | var stats = this.stats; 82 | 83 | function draw(type, n) { 84 | write(' '); 85 | write(Base.color(type, n)); 86 | write('\n'); 87 | } 88 | 89 | draw('green', stats.passes); 90 | draw('fail', stats.failures); 91 | draw('pending', stats.pending); 92 | write('\n'); 93 | 94 | this.cursorUp(this.numberOfLines); 95 | }; 96 | 97 | /** 98 | * Append the rainbow. 99 | * 100 | * @api private 101 | */ 102 | 103 | NyanCat.prototype.appendRainbow = function(){ 104 | var segment = this.tick ? '_' : '-'; 105 | var rainbowified = this.rainbowify(segment); 106 | 107 | for (var index = 0; index < this.numberOfLines; index++) { 108 | var trajectory = this.trajectories[index]; 109 | if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift(); 110 | trajectory.push(rainbowified); 111 | } 112 | }; 113 | 114 | /** 115 | * Draw the rainbow. 116 | * 117 | * @api private 118 | */ 119 | 120 | NyanCat.prototype.drawRainbow = function(){ 121 | var self = this; 122 | 123 | this.trajectories.forEach(function(line, index) { 124 | write('\u001b[' + self.scoreboardWidth + 'C'); 125 | write(line.join('')); 126 | write('\n'); 127 | }); 128 | 129 | this.cursorUp(this.numberOfLines); 130 | }; 131 | 132 | /** 133 | * Draw the nyan cat 134 | * 135 | * @api private 136 | */ 137 | 138 | NyanCat.prototype.drawNyanCat = function() { 139 | var self = this; 140 | var startWidth = this.scoreboardWidth + this.trajectories[0].length; 141 | var dist = '\u001b[' + startWidth + 'C'; 142 | var padding = ''; 143 | 144 | write(dist); 145 | write('_,------,'); 146 | write('\n'); 147 | 148 | write(dist); 149 | padding = self.tick ? ' ' : ' '; 150 | write('_|' + padding + '/\\_/\\ '); 151 | write('\n'); 152 | 153 | write(dist); 154 | padding = self.tick ? '_' : '__'; 155 | var tail = self.tick ? '~' : '^'; 156 | var face; 157 | write(tail + '|' + padding + this.face() + ' '); 158 | write('\n'); 159 | 160 | write(dist); 161 | padding = self.tick ? ' ' : ' '; 162 | write(padding + '"" "" '); 163 | write('\n'); 164 | 165 | this.cursorUp(this.numberOfLines); 166 | }; 167 | 168 | /** 169 | * Draw nyan cat face. 170 | * 171 | * @return {String} 172 | * @api private 173 | */ 174 | 175 | NyanCat.prototype.face = function() { 176 | var stats = this.stats; 177 | if (stats.failures) { 178 | return '( x .x)'; 179 | } else if (stats.pending) { 180 | return '( o .o)'; 181 | } else if(stats.passes) { 182 | return '( ^ .^)'; 183 | } else { 184 | return '( - .-)'; 185 | } 186 | }; 187 | 188 | /** 189 | * Move cursor up `n`. 190 | * 191 | * @param {Number} n 192 | * @api private 193 | */ 194 | 195 | NyanCat.prototype.cursorUp = function(n) { 196 | write('\u001b[' + n + 'A'); 197 | }; 198 | 199 | /** 200 | * Move cursor down `n`. 201 | * 202 | * @param {Number} n 203 | * @api private 204 | */ 205 | 206 | NyanCat.prototype.cursorDown = function(n) { 207 | write('\u001b[' + n + 'B'); 208 | }; 209 | 210 | /** 211 | * Generate rainbow colors. 212 | * 213 | * @return {Array} 214 | * @api private 215 | */ 216 | 217 | NyanCat.prototype.generateColors = function(){ 218 | var colors = []; 219 | 220 | for (var i = 0; i < (6 * 7); i++) { 221 | var pi3 = Math.floor(Math.PI / 3); 222 | var n = (i * (1.0 / 6)); 223 | var r = Math.floor(3 * Math.sin(n) + 3); 224 | var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); 225 | var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); 226 | colors.push(36 * r + 6 * g + b + 16); 227 | } 228 | 229 | return colors; 230 | }; 231 | 232 | /** 233 | * Apply rainbow to the given `str`. 234 | * 235 | * @param {String} str 236 | * @return {String} 237 | * @api private 238 | */ 239 | 240 | NyanCat.prototype.rainbowify = function(str){ 241 | if (!Base.useColors) 242 | return str; 243 | var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; 244 | this.colorIndex += 1; 245 | return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; 246 | }; 247 | 248 | /** 249 | * Stdout helper. 250 | */ 251 | 252 | function write(string) { 253 | process.stdout.write(string); 254 | } 255 | 256 | /** 257 | * Inherit from `Base.prototype`. 258 | */ 259 | 260 | Object.setPrototypeOf(NyanCat.prototype, Base.prototype); 261 | -------------------------------------------------------------------------------- /lib/reporters/progress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , cursor = Base.cursor 7 | , color = Base.color; 8 | 9 | /** 10 | * Expose `Progress`. 11 | */ 12 | 13 | exports = module.exports = Progress; 14 | 15 | /** 16 | * General progress bar color. 17 | */ 18 | 19 | Base.colors.progress = 90; 20 | 21 | /** 22 | * Initialize a new `Progress` bar test reporter. 23 | * 24 | * @param {Runner} runner 25 | * @param {Object} options 26 | * @api public 27 | */ 28 | 29 | function Progress(runner, options) { 30 | Base.call(this, runner); 31 | 32 | var self = this 33 | , options = options || {} 34 | , stats = this.stats 35 | , width = Base.window.width * .50 | 0 36 | , total = runner.total 37 | , complete = 0 38 | , max = Math.max 39 | , lastN = -1; 40 | 41 | // default chars 42 | options.open = options.open || '['; 43 | options.complete = options.complete || '▬'; 44 | options.incomplete = options.incomplete || Base.symbols.dot; 45 | options.close = options.close || ']'; 46 | options.verbose = false; 47 | 48 | // tests started 49 | runner.on('start', function(){ 50 | console.log(); 51 | cursor.hide(); 52 | }); 53 | 54 | // tests complete 55 | runner.on('test end', function(){ 56 | complete++; 57 | var incomplete = total - complete 58 | , percent = complete / total 59 | , n = width * percent | 0 60 | , i = width - n; 61 | 62 | if (lastN === n && !options.verbose) { 63 | // Don't re-render the line if it hasn't changed 64 | return; 65 | } 66 | lastN = n; 67 | 68 | cursor.CR(); 69 | process.stdout.write('\u001b[J'); 70 | process.stdout.write(color('progress', ' ' + options.open)); 71 | process.stdout.write(Array(n).join(options.complete)); 72 | process.stdout.write(Array(i).join(options.incomplete)); 73 | process.stdout.write(color('progress', options.close)); 74 | if (options.verbose) { 75 | process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); 76 | } 77 | }); 78 | 79 | // tests are complete, output some stats 80 | // and the failures if any 81 | runner.on('end', function(){ 82 | cursor.show(); 83 | console.log(); 84 | self.epilogue(); 85 | }); 86 | } 87 | 88 | /** 89 | * Inherit from `Base.prototype`. 90 | */ 91 | 92 | Object.setPrototypeOf(Progress.prototype, Base.prototype); 93 | -------------------------------------------------------------------------------- /lib/reporters/silent.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = function () {} 2 | -------------------------------------------------------------------------------- /lib/reporters/spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , cursor = Base.cursor 7 | , color = Base.color; 8 | 9 | /** 10 | * Expose `Spec`. 11 | */ 12 | 13 | exports = module.exports = Spec; 14 | 15 | /** 16 | * Initialize a new `Spec` test reporter. 17 | * 18 | * @param {Runner} runner 19 | * @api public 20 | */ 21 | 22 | function Spec(runner) { 23 | Base.call(this, runner); 24 | 25 | var self = this 26 | , stats = this.stats 27 | , indents = 0 28 | , n = 0; 29 | 30 | function indent() { 31 | return Array(indents).join(' ') 32 | } 33 | 34 | runner.on('start', function(){ 35 | console.log(); 36 | }); 37 | 38 | runner.on('suite', function(suite){ 39 | ++indents; 40 | console.log(color('suite', '%s%s'), indent(), suite.title); 41 | }); 42 | 43 | runner.on('suite end', function(suite){ 44 | --indents; 45 | if (1 == indents) console.log(); 46 | }); 47 | 48 | runner.on('pending', function(test){ 49 | var fmt = indent() + color('pending', ' - %s'); 50 | console.log(fmt, test.title); 51 | }); 52 | 53 | runner.on('pass', function(test){ 54 | if ('fast' == test.speed) { 55 | var fmt = indent() 56 | + color('checkmark', ' ' + Base.symbols.ok) 57 | + color('pass', ' %s'); 58 | cursor.CR(); 59 | console.log(fmt, test.title); 60 | } else { 61 | var fmt = indent() 62 | + color('checkmark', ' ' + Base.symbols.ok) 63 | + color('pass', ' %s') 64 | + color(test.speed, ' (%dms)'); 65 | cursor.CR(); 66 | console.log(fmt, test.title, test.duration); 67 | } 68 | }); 69 | 70 | runner.on('fail', function(test, err){ 71 | cursor.CR(); 72 | console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); 73 | }); 74 | 75 | runner.on('end', self.epilogue.bind(self)); 76 | } 77 | 78 | /** 79 | * Inherit from `Base.prototype`. 80 | */ 81 | 82 | Object.setPrototypeOf(Spec.prototype, Base.prototype); 83 | -------------------------------------------------------------------------------- /lib/reporters/templates/coverage.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Coverage 5 | meta(charset='utf-8') 6 | include script.html 7 | include style.html 8 | body 9 | #coverage 10 | h1#overview Coverage 11 | include menu 12 | 13 | #stats(class=coverageClass(cov.coverage)) 14 | .percentage #{cov.coverage | 0}% 15 | .sloc= cov.sloc 16 | .hits= cov.hits 17 | .misses= cov.misses 18 | 19 | #files 20 | for file in cov.files 21 | .file 22 | h2(id=file.filename)= file.filename 23 | #stats(class=coverageClass(file.coverage)) 24 | .percentage #{file.coverage | 0}% 25 | .sloc= file.sloc 26 | .hits= file.hits 27 | .misses= file.misses 28 | 29 | table#source 30 | thead 31 | tr 32 | th Line 33 | th Hits 34 | th Source 35 | tbody 36 | for line, number in file.source 37 | if line.coverage > 0 38 | tr.hit 39 | td.line= number 40 | td.hits= line.coverage 41 | td.source= line.source 42 | else if 0 === line.coverage 43 | tr.miss 44 | td.line= number 45 | td.hits 0 46 | td.source= line.source 47 | else 48 | tr 49 | td.line= number 50 | td.hits 51 | td.source= line.source || ' ' 52 | -------------------------------------------------------------------------------- /lib/reporters/templates/menu.jade: -------------------------------------------------------------------------------- 1 | #menu 2 | li 3 | a(href='#overview') overview 4 | for file in cov.files 5 | li 6 | span.cov(class=coverageClass(file.coverage)) #{file.coverage | 0} 7 | a(href='##{file.filename}') 8 | segments = file.filename.split('/') 9 | basename = segments.pop() 10 | if segments.length 11 | span.dirname= segments.join('/') + '/' 12 | span.basename= basename 13 | a#logo(href='http://visionmedia.github.io/mocha/') m 14 | -------------------------------------------------------------------------------- /lib/reporters/templates/script.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /lib/reporters/templates/style.html: -------------------------------------------------------------------------------- 1 | 325 | -------------------------------------------------------------------------------- /lib/reporters/xunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var Base = require('./base') 6 | , utils = require('../utils') 7 | , fs = require('fs') 8 | , escape = utils.escape; 9 | 10 | /** 11 | * Save timer references to avoid Sinon interfering (see GH-237). 12 | */ 13 | 14 | var Date = global.Date 15 | , setTimeout = global.setTimeout 16 | , setInterval = global.setInterval 17 | , clearTimeout = global.clearTimeout 18 | , clearInterval = global.clearInterval; 19 | 20 | /** 21 | * Expose `XUnit`. 22 | */ 23 | 24 | exports = module.exports = XUnit; 25 | 26 | /** 27 | * Initialize a new `XUnit` reporter. 28 | * 29 | * @param {Runner} runner 30 | * @api public 31 | */ 32 | 33 | function XUnit(runner, options) { 34 | Base.call(this, runner); 35 | var stats = this.stats 36 | , tests = [] 37 | , self = this; 38 | 39 | if (options.reporterOptions && options.reporterOptions.output) { 40 | if (! fs.createWriteStream) { 41 | throw new Error('file output not supported in browser'); 42 | } 43 | self.fileStream = fs.createWriteStream(options.reporterOptions.output); 44 | } 45 | 46 | runner.on('pending', function(test){ 47 | tests.push(test); 48 | }); 49 | 50 | runner.on('pass', function(test){ 51 | tests.push(test); 52 | }); 53 | 54 | runner.on('fail', function(test){ 55 | tests.push(test); 56 | }); 57 | 58 | runner.on('end', function(){ 59 | self.write(tag('testsuite', { 60 | name: 'TAP Tests' 61 | , tests: stats.tests 62 | , failures: stats.failures 63 | , errors: stats.failures 64 | , skipped: stats.tests - stats.failures - stats.passes 65 | , timestamp: (new Date).toUTCString() 66 | , time: (stats.duration / 1000) || 0 67 | }, false)); 68 | 69 | tests.forEach(function(t) { self.test(t); }); 70 | self.write(''); 71 | }); 72 | } 73 | 74 | /** 75 | * Override done to close the stream (if it's a file). 76 | */ 77 | XUnit.prototype.done = function(failures, fn) { 78 | if (this.fileStream) { 79 | this.fileStream.end(function() { 80 | fn(failures); 81 | }); 82 | } else { 83 | fn(failures); 84 | } 85 | }; 86 | 87 | /** 88 | * Inherit from `Base.prototype`. 89 | */ 90 | 91 | Object.setPrototypeOf(XUnit.prototype, Base.prototype); 92 | 93 | /** 94 | * Write out the given line 95 | */ 96 | XUnit.prototype.write = function(line) { 97 | if (this.fileStream) { 98 | this.fileStream.write(line + '\n'); 99 | } else { 100 | console.log(line); 101 | } 102 | }; 103 | 104 | /** 105 | * Output tag for the given `test.` 106 | */ 107 | 108 | XUnit.prototype.test = function(test, ostream) { 109 | var attrs = { 110 | classname: test.parent.fullTitle() 111 | , name: test.title 112 | , time: (test.duration / 1000) || 0 113 | }; 114 | 115 | if ('failed' == test.state) { 116 | var err = test.err; 117 | this.write(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack)))); 118 | } else if (test.pending) { 119 | this.write(tag('testcase', attrs, false, tag('skipped', {}, true))); 120 | } else { 121 | this.write(tag('testcase', attrs, true) ); 122 | } 123 | }; 124 | 125 | /** 126 | * HTML tag helper. 127 | */ 128 | 129 | function tag(name, attrs, close, content) { 130 | var end = close ? '/>' : '>' 131 | , pairs = [] 132 | , tag; 133 | 134 | for (var key in attrs) { 135 | pairs.push(key + '="' + escape(attrs[key]) + '"'); 136 | } 137 | 138 | tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; 139 | if (content) tag += content + ''; 149 | } 150 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | // A facade from the tap-parser to the Mocha "Runner" object. 2 | // Note that pass/fail/suite events need to also mock the "Runnable" 3 | // objects (either "Suite" or "Test") since these have functions 4 | // which are called by the formatters. 5 | 6 | module.exports = Runner 7 | 8 | // relevant events: 9 | // 10 | // start() 11 | // Start of the top-level test set 12 | // 13 | // end() 14 | // End of the top-level test set. 15 | // 16 | // fail(test, err) 17 | // any "not ok" test that is not the trailing test for a suite 18 | // of >0 test points. 19 | // 20 | // pass(test) 21 | // any "ok" test point that is not the trailing test for a suite 22 | // of >0 tests 23 | // 24 | // pending(test) 25 | // Any "todo" test 26 | // 27 | // suite(suite) 28 | // A suite is a child test with >0 test points. This is a little bit 29 | // tricky, because TAP will provide a "child" event before we know 30 | // that it's a "suite". We see the "# Subtest: name" comment as the 31 | // first thing in the subtest. Then, when we get our first test point, 32 | // we know that it's a suite, and can emit the event with the mock suite. 33 | // 34 | // suite end(suite) 35 | // Emitted when we end the subtest 36 | // 37 | // test(test) 38 | // Any test point which is not the trailing test for a suite. 39 | // 40 | // test end(test) 41 | // Emitted immediately after the "test" event because test points are 42 | // not async in TAP. 43 | 44 | var util = require('util') 45 | var Test = require('./test.js') 46 | var Suite = require('./suite.js') 47 | var Writable = require('stream').Writable 48 | var Parser = require('tap-parser') 49 | 50 | // $1 = number, $2 = units 51 | var timere = /^#\s*time=((?:0|[1-9][0-9]*?)(?:\.[0-9]+)?)(ms|s)?$/ 52 | 53 | util.inherits(Runner, Writable) 54 | 55 | function Runner (options) { 56 | if (!(this instanceof Runner)) 57 | return new Runner(options) 58 | 59 | var parser = this.parser = new Parser(options) 60 | this.startTime = new Date() 61 | 62 | attachEvents(this, parser, 0) 63 | Writable.call(this, options) 64 | } 65 | 66 | Runner.prototype.write = function () { 67 | if (!this.emittedStart) { 68 | this.emittedStart = true 69 | this.emit('start') 70 | } 71 | 72 | return this.parser.write.apply(this.parser, arguments) 73 | } 74 | 75 | Runner.prototype.end = function () { 76 | return this.parser.end.apply(this.parser, arguments) 77 | } 78 | 79 | Parser.prototype.fullTitle = function () { 80 | if (!this.parent) 81 | return this.name || '' 82 | else 83 | return (this.parent.fullTitle() + ' ' + (this.name || '')).trim() 84 | } 85 | 86 | function attachEvents (runner, parser, level) { 87 | parser.runner = runner 88 | 89 | if (level === 0) { 90 | parser.on('line', function (c) { 91 | runner.emit('line', c) 92 | }) 93 | parser.on('version', function (v) { 94 | runner.emit('version', v) 95 | }) 96 | parser.on('complete', function (res) { 97 | runner.emit('end') 98 | }) 99 | parser.on('comment', function (c) { 100 | var tmatch = c.trim().match(timere) 101 | if (tmatch) { 102 | var t = +tmatch[1] 103 | if (tmatch[2] === 's') 104 | t *= 1000 105 | parser.time = t 106 | if (runner.stats) 107 | runner.stats.duration = t 108 | } 109 | }) 110 | } 111 | 112 | parser.emittedSuite = false 113 | parser.didAssert = false 114 | parser.name = parser.name || '' 115 | parser.doingChild = null 116 | 117 | parser.on('complete', function (res) { 118 | if (!res.ok) { 119 | var fail = { ok: false, diag: {} } 120 | var count = res.count 121 | if (res.plan) { 122 | var plan = res.plan.end - res.plan.start + 1 123 | if (count !== plan) { 124 | fail.name = 'test count !== plan' 125 | fail.diag = { 126 | found: count, 127 | wanted: plan 128 | } 129 | } else { 130 | // probably handled on child parser 131 | return 132 | } 133 | } else { 134 | fail.name = 'missing plan' 135 | } 136 | fail.diag.results = res 137 | emitTest(parser, fail) 138 | } 139 | }) 140 | 141 | parser.on('child', function (child) { 142 | child.parent = parser 143 | attachEvents(runner, child, level + 1) 144 | 145 | // if we're in a suite, but we haven't emitted it yet, then we 146 | // know that an assert will follow this child, even if there are 147 | // no others. That means that we will definitely have a 'suite' 148 | // event to emit. 149 | emitSuite(this) 150 | 151 | this.didAssert = true 152 | this.doingChild = child 153 | }) 154 | 155 | if (!parser.name) { 156 | parser.on('comment', function (c) { 157 | if (!this.name && c.match(/^# Subtest: /)) { 158 | c = c.trim().replace(/^# Subtest: /, '') 159 | this.name = c 160 | } 161 | }) 162 | } 163 | 164 | // Just dump all non-parsing stuff to stderr 165 | parser.on('extra', function (c) { 166 | process.stderr.write(c) 167 | }) 168 | 169 | parser.on('assert', function (result) { 170 | emitSuite(this) 171 | 172 | // no need to print the trailing assert for subtests 173 | // we've already emitted a 'suite end' event for this. 174 | // UNLESS, there were no other asserts, AND it's root level 175 | if (this.doingChild) { 176 | var suite = this.doingChild.suite 177 | if (this.doingChild.name === result.name) { 178 | if (suite) { 179 | if (result.time) 180 | suite.duration = result.time 181 | 182 | // If it's ok so far, but the ending result is not-ok, then 183 | // that means that it exited non-zero. Emit the test so 184 | // that we can print it as a failure. 185 | if (suite.ok && !result.ok) 186 | emitTest(this, result) 187 | } 188 | } 189 | 190 | var emitOn = this 191 | var dc = this.doingChild 192 | this.doingChild = null 193 | 194 | if (!dc.didAssert && dc.level === 1) { 195 | emitOn = dc 196 | } else if (dc.didAssert) { 197 | if (dc.suite) 198 | runner.emit('suite end', dc.suite) 199 | return 200 | } else { 201 | emitOn = this 202 | } 203 | 204 | emitSuite(emitOn) 205 | emitTest(emitOn, result) 206 | if (emitOn !== this && emitOn.suite) { 207 | runner.emit('suite end', emitOn.suite) 208 | delete emitOn.suite 209 | } 210 | if (dc.suite) { 211 | runner.emit('suite end', dc.suite) 212 | } 213 | return 214 | } 215 | 216 | this.didAssert = true 217 | this.doingChild = null 218 | 219 | emitTest(this, result) 220 | }) 221 | 222 | parser.on('complete', function (results) { 223 | this.results = results 224 | }) 225 | 226 | parser.on('bailout', function (reason) { 227 | var suite = this.suite 228 | runner.emit('bailout', reason, suite) 229 | if (suite) 230 | this.suite = suite.parent 231 | }) 232 | 233 | // proxy all stream events directly 234 | var streamEvents = [ 235 | 'pipe', 'prefinish', 'finish', 'unpipe', 'close' 236 | ] 237 | 238 | streamEvents.forEach(function (ev) { 239 | parser.on(ev, function () { 240 | var args = [ev] 241 | args.push.apply(args, arguments) 242 | runner.emit.apply(runner, args) 243 | }) 244 | }) 245 | } 246 | 247 | function emitSuite (parser) { 248 | if (!parser.emittedSuite && parser.name) { 249 | parser.emittedSuite = true 250 | var suite = parser.suite = new Suite(parser) 251 | if (parser.parent && parser.parent.suite) 252 | parser.parent.suite.suites.push(suite) 253 | if (parser.runner.stats) 254 | parser.runner.stats.suites ++ 255 | 256 | parser.runner.emit('suite', suite) 257 | } 258 | } 259 | 260 | function emitTest (parser, result) { 261 | var runner = parser.runner 262 | var test = new Test(result, parser) 263 | 264 | if (parser.suite) { 265 | parser.suite.tests.push(test) 266 | if (!result.ok) { 267 | for (var p = parser; p && p.suite; p = p.parent) { 268 | p.suite.ok = false 269 | } 270 | } 271 | parser.suite.ok = parser.suite.ok && result.ok 272 | } 273 | 274 | runner.emit('test', test) 275 | if (result.skip || result.todo) { 276 | runner.emit('pending', test) 277 | } else if (result.ok) { 278 | runner.emit('pass', test) 279 | } else { 280 | var error = getError(result) 281 | runner.emit('fail', test, error) 282 | } 283 | runner.emit('test end', test) 284 | } 285 | 286 | function getError (result) { 287 | var err 288 | 289 | function reviveStack (stack) { 290 | if (!stack) 291 | return null 292 | 293 | return stack.trim().split('\n').map(function (line) { 294 | return ' at ' + line 295 | }).join('\n') 296 | } 297 | 298 | if (result.diag && result.diag.error) { 299 | // see: https://github.com/tapjs/tap-mocha-reporter/issues/72 300 | if (typeof result.diag.error === 'string') { 301 | err = { 302 | name: 'Error', 303 | message: result.diag.error, 304 | toString: function () { 305 | return this.name + ': ' + this.message 306 | }, 307 | stack: result.diag.stack 308 | } 309 | } else { 310 | err = { 311 | name: result.diag.error.name || 'Error', 312 | message: result.diag.error.message, 313 | toString: function () { 314 | return this.name + ': ' + this.message 315 | }, 316 | stack: result.diag.error.stack 317 | } 318 | } 319 | } else { 320 | err = { 321 | message: (result.name || '(unnamed error)').replace(/^Error: /, ''), 322 | toString: function () { 323 | return 'Error: ' + this.message 324 | }, 325 | stack: result.diag && (result.diag.stack || result.diag.at) 326 | } 327 | } 328 | 329 | var diag = result.diag 330 | 331 | if (err.stack) 332 | err.stack = (diag.values ? JSON.stringify(diag.values) : err.toString()) + '\n' + reviveStack(err.stack) 333 | 334 | if (diag) { 335 | var hasFound = diag.hasOwnProperty('found') 336 | var hasWanted = diag.hasOwnProperty('wanted') 337 | var hasDiff = diag.hasOwnProperty('diff') 338 | 339 | if (hasDiff) 340 | err.diff = diag.diff 341 | 342 | if (hasFound) 343 | err.actual = diag.found 344 | 345 | if (hasWanted) 346 | err.expected = diag.wanted 347 | 348 | if (hasFound && hasWanted || hasDiff) 349 | err.showDiff = true 350 | } 351 | 352 | return err 353 | } 354 | -------------------------------------------------------------------------------- /lib/suite.js: -------------------------------------------------------------------------------- 1 | // minimal mock of mocha's Suite class for formatters 2 | 3 | module.exports = Suite 4 | 5 | function Suite (parent) { 6 | if (!parent.parent || !parent.parent.emittedSuite) 7 | this.root = true 8 | else 9 | this.root = false 10 | 11 | this.title = parent.name || '' 12 | this.suites = [] 13 | this.tests = [] 14 | this.ok = true 15 | } 16 | 17 | Suite.prototype.fullTitle = function () { 18 | if (!this.parent) 19 | return (this.title || '').trim() 20 | else 21 | return (this.parent.fullTitle() + ' ' + (this.title || '')).trim() 22 | } 23 | -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | // minimal mock of the mocha Test class for formatters 2 | 3 | module.exports = Test 4 | 5 | function Test (result, parent) { 6 | this.result = result 7 | this._slow = 75 8 | this.duration = result.time 9 | this.title = result.name 10 | this.state = result.ok ? 'pass' : 'failed' 11 | this.pending = result.todo || result.skip || false 12 | if (result.diag && result.diag.source) { 13 | var source = result.diag.source 14 | this.fn = { 15 | toString: function () { 16 | return 'function(){' + source + '\n}' 17 | } 18 | } 19 | } 20 | 21 | Object.defineProperty(this, 'parent', { 22 | value: parent, 23 | writable: true, 24 | configurable: true, 25 | enumerable: false 26 | }) 27 | } 28 | 29 | Test.prototype.fullTitle = function () { 30 | return (this.parent.fullTitle() + ' ' + (this.title || '')).trim() 31 | } 32 | 33 | Test.prototype.slow = function (ms){ 34 | return 75 35 | } 36 | 37 | Test.prototype.fn = { 38 | toString: function () { 39 | return 'function () {\n}' 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var fs = require('fs') 6 | , path = require('path') 7 | , basename = path.basename 8 | , exists = fs.existsSync || path.existsSync 9 | , glob = require('glob') 10 | , join = path.join 11 | , debug = require('debug')('mocha:watch'); 12 | 13 | /** 14 | * Ignored directories. 15 | */ 16 | 17 | var ignore = ['node_modules', '.git']; 18 | 19 | /** 20 | * Escape special characters in the given string of html. 21 | * 22 | * @param {String} html 23 | * @return {String} 24 | * @api private 25 | */ 26 | 27 | exports.escape = function(html){ 28 | return String(html) 29 | .replace(/&/g, '&') 30 | .replace(/"/g, '"') 31 | .replace(//g, '>'); 33 | }; 34 | 35 | /** 36 | * Array#forEach (<=IE8) 37 | * 38 | * @param {Array} array 39 | * @param {Function} fn 40 | * @param {Object} scope 41 | * @api private 42 | */ 43 | 44 | exports.forEach = function(arr, fn, scope){ 45 | for (var i = 0, l = arr.length; i < l; i++) 46 | fn.call(scope, arr[i], i); 47 | }; 48 | 49 | /** 50 | * Array#map (<=IE8) 51 | * 52 | * @param {Array} array 53 | * @param {Function} fn 54 | * @param {Object} scope 55 | * @api private 56 | */ 57 | 58 | exports.map = function(arr, fn, scope){ 59 | var result = []; 60 | for (var i = 0, l = arr.length; i < l; i++) 61 | result.push(fn.call(scope, arr[i], i, arr)); 62 | return result; 63 | }; 64 | 65 | /** 66 | * Array#indexOf (<=IE8) 67 | * 68 | * @parma {Array} arr 69 | * @param {Object} obj to find index of 70 | * @param {Number} start 71 | * @api private 72 | */ 73 | 74 | exports.indexOf = function(arr, obj, start){ 75 | for (var i = start || 0, l = arr.length; i < l; i++) { 76 | if (arr[i] === obj) 77 | return i; 78 | } 79 | return -1; 80 | }; 81 | 82 | /** 83 | * Array#reduce (<=IE8) 84 | * 85 | * @param {Array} array 86 | * @param {Function} fn 87 | * @param {Object} initial value 88 | * @api private 89 | */ 90 | 91 | exports.reduce = function(arr, fn, val){ 92 | var rval = val; 93 | 94 | for (var i = 0, l = arr.length; i < l; i++) { 95 | rval = fn(rval, arr[i], i, arr); 96 | } 97 | 98 | return rval; 99 | }; 100 | 101 | /** 102 | * Array#filter (<=IE8) 103 | * 104 | * @param {Array} array 105 | * @param {Function} fn 106 | * @api private 107 | */ 108 | 109 | exports.filter = function(arr, fn){ 110 | var ret = []; 111 | 112 | for (var i = 0, l = arr.length; i < l; i++) { 113 | var val = arr[i]; 114 | if (fn(val, i, arr)) ret.push(val); 115 | } 116 | 117 | return ret; 118 | }; 119 | 120 | /** 121 | * Object.keys (<=IE8) 122 | * 123 | * @param {Object} obj 124 | * @return {Array} keys 125 | * @api private 126 | */ 127 | 128 | exports.keys = Object.keys || function(obj) { 129 | var keys = [] 130 | , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8 131 | 132 | for (var key in obj) { 133 | if (has.call(obj, key)) { 134 | keys.push(key); 135 | } 136 | } 137 | 138 | return keys; 139 | }; 140 | 141 | /** 142 | * Watch the given `files` for changes 143 | * and invoke `fn(file)` on modification. 144 | * 145 | * @param {Array} files 146 | * @param {Function} fn 147 | * @api private 148 | */ 149 | 150 | exports.watch = function(files, fn){ 151 | var options = { interval: 100 }; 152 | files.forEach(function(file){ 153 | debug('file %s', file); 154 | fs.watchFile(file, options, function(curr, prev){ 155 | if (prev.mtime < curr.mtime) fn(file); 156 | }); 157 | }); 158 | }; 159 | 160 | /** 161 | * Array.isArray (<=IE8) 162 | * 163 | * @param {Object} obj 164 | * @return {Boolean} 165 | * @api private 166 | */ 167 | var isArray = Array.isArray || function (obj) { 168 | return '[object Array]' == {}.toString.call(obj); 169 | }; 170 | 171 | /** 172 | * @description 173 | * Buffer.prototype.toJSON polyfill 174 | * @type {Function} 175 | */ 176 | if(typeof Buffer !== 'undefined' && Buffer.prototype) { 177 | Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () { 178 | return Array.prototype.slice.call(this, 0); 179 | }; 180 | } 181 | 182 | /** 183 | * Ignored files. 184 | */ 185 | 186 | function ignored(path){ 187 | return !~ignore.indexOf(path); 188 | } 189 | 190 | /** 191 | * Lookup files in the given `dir`. 192 | * 193 | * @return {Array} 194 | * @api private 195 | */ 196 | 197 | exports.files = function(dir, ext, ret){ 198 | ret = ret || []; 199 | ext = ext || ['js']; 200 | 201 | var re = new RegExp('\\.(' + ext.join('|') + ')$'); 202 | 203 | fs.readdirSync(dir) 204 | .filter(ignored) 205 | .forEach(function(path){ 206 | path = join(dir, path); 207 | if (fs.statSync(path).isDirectory()) { 208 | exports.files(path, ext, ret); 209 | } else if (path.match(re)) { 210 | ret.push(path); 211 | } 212 | }); 213 | 214 | return ret; 215 | }; 216 | 217 | /** 218 | * Compute a slug from the given `str`. 219 | * 220 | * @param {String} str 221 | * @return {String} 222 | * @api private 223 | */ 224 | 225 | exports.slug = function(str){ 226 | return str 227 | .toLowerCase() 228 | .replace(/ +/g, '-') 229 | .replace(/[^-\w]/g, ''); 230 | }; 231 | 232 | /** 233 | * Strip the function definition from `str`, 234 | * and re-indent for pre whitespace. 235 | */ 236 | 237 | exports.clean = function(str) { 238 | str = str 239 | .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '') 240 | .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '') 241 | .replace(/\s+\}$/, ''); 242 | 243 | var spaces = str.match(/^\n?( *)/)[1].length 244 | , tabs = str.match(/^\n?(\t*)/)[1].length 245 | , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm'); 246 | 247 | str = str.replace(re, ''); 248 | 249 | return exports.trim(str); 250 | }; 251 | 252 | /** 253 | * Trim the given `str`. 254 | * 255 | * @param {String} str 256 | * @return {String} 257 | * @api private 258 | */ 259 | 260 | exports.trim = function(str){ 261 | return str.trim() 262 | }; 263 | 264 | /** 265 | * Parse the given `qs`. 266 | * 267 | * @param {String} qs 268 | * @return {Object} 269 | * @api private 270 | */ 271 | 272 | exports.parseQuery = function(qs){ 273 | return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){ 274 | var i = pair.indexOf('=') 275 | , key = pair.slice(0, i) 276 | , val = pair.slice(++i); 277 | 278 | obj[key] = decodeURIComponent(val); 279 | return obj; 280 | }, {}); 281 | }; 282 | 283 | /** 284 | * Highlight the given string of `js`. 285 | * 286 | * @param {String} js 287 | * @return {String} 288 | * @api private 289 | */ 290 | 291 | function highlight(js) { 292 | return js 293 | .replace(//g, '>') 295 | .replace(/\/\/(.*)/gm, '//$1') 296 | .replace(/('.*?')/gm, '$1') 297 | .replace(/(\d+\.\d+)/gm, '$1') 298 | .replace(/(\d+)/gm, '$1') 299 | .replace(/\bnew[ \t]+(\w+)/gm, 'new $1') 300 | .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1') 301 | } 302 | 303 | /** 304 | * Highlight the contents of tag `name`. 305 | * 306 | * @param {String} name 307 | * @api private 308 | */ 309 | 310 | exports.highlightTags = function(name) { 311 | var code = document.getElementById('mocha').getElementsByTagName(name); 312 | for (var i = 0, len = code.length; i < len; ++i) { 313 | code[i].innerHTML = highlight(code[i].innerHTML); 314 | } 315 | }; 316 | 317 | /** 318 | * If a value could have properties, and has none, this function is called, which returns 319 | * a string representation of the empty value. 320 | * 321 | * Functions w/ no properties return `'[Function]'` 322 | * Arrays w/ length === 0 return `'[]'` 323 | * Objects w/ no properties return `'{}'` 324 | * All else: return result of `value.toString()` 325 | * 326 | * @param {*} value Value to inspect 327 | * @param {string} [type] The type of the value, if known. 328 | * @returns {string} 329 | */ 330 | var emptyRepresentation = function emptyRepresentation(value, type) { 331 | type = type || exports.type(value); 332 | 333 | switch(type) { 334 | case 'function': 335 | return '[Function]'; 336 | case 'object': 337 | return '{}'; 338 | case 'array': 339 | return '[]'; 340 | default: 341 | return value.toString(); 342 | } 343 | }; 344 | 345 | /** 346 | * Takes some variable and asks `{}.toString()` what it thinks it is. 347 | * @param {*} value Anything 348 | * @example 349 | * type({}) // 'object' 350 | * type([]) // 'array' 351 | * type(1) // 'number' 352 | * type(false) // 'boolean' 353 | * type(Infinity) // 'number' 354 | * type(null) // 'null' 355 | * type(new Date()) // 'date' 356 | * type(/foo/) // 'regexp' 357 | * type('type') // 'string' 358 | * type(global) // 'global' 359 | * @api private 360 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString 361 | * @returns {string} 362 | */ 363 | exports.type = function type(value) { 364 | if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { 365 | return 'buffer'; 366 | } 367 | return Object.prototype.toString.call(value) 368 | .replace(/^\[.+\s(.+?)\]$/, '$1') 369 | .toLowerCase(); 370 | }; 371 | 372 | /** 373 | * @summary Stringify `value`. 374 | * @description Different behavior depending on type of value. 375 | * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively. 376 | * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes. 377 | * - If `value` is an *empty* object, function, or array, return result of function 378 | * {@link emptyRepresentation}. 379 | * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of 380 | * JSON.stringify(). 381 | * 382 | * @see exports.type 383 | * @param {*} value 384 | * @return {string} 385 | * @api private 386 | */ 387 | 388 | exports.stringify = function(value) { 389 | var type = exports.type(value); 390 | 391 | if (!~exports.indexOf(['object', 'array', 'function'], type)) { 392 | if(type != 'buffer') { 393 | return jsonStringify(value); 394 | } 395 | var json = value.toJSON(); 396 | // Based on the toJSON result 397 | return jsonStringify(json.data && json.type ? json.data : json, 2) 398 | .replace(/,(\n|$)/g, '$1'); 399 | } 400 | 401 | for (var prop in value) { 402 | if (Object.prototype.hasOwnProperty.call(value, prop)) { 403 | return jsonStringify(exports.canonicalize(value), 2) 404 | .replace(/,(\n|$)/g, '$1'); 405 | } 406 | } 407 | 408 | return emptyRepresentation(value, type); 409 | }; 410 | 411 | /** 412 | * @description 413 | * like JSON.stringify but more sense. 414 | * @param {Object} object 415 | * @param {Number=} spaces 416 | * @param {number=} depth 417 | * @returns {*} 418 | * @private 419 | */ 420 | function jsonStringify(object, spaces, depth) { 421 | if(typeof spaces == 'undefined') return _stringify(object); // primitive types 422 | 423 | depth = depth || 1; 424 | var space = spaces * depth 425 | , str = isArray(object) ? '[' : '{' 426 | , end = isArray(object) ? ']' : '}' 427 | , length = object.length || exports.keys(object).length 428 | , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill 429 | 430 | function _stringify(val) { 431 | switch (exports.type(val)) { 432 | case 'null': 433 | case 'undefined': 434 | val = '[' + val + ']'; 435 | break; 436 | case 'array': 437 | case 'object': 438 | val = jsonStringify(val, spaces, depth + 1); 439 | break; 440 | case 'boolean': 441 | case 'regexp': 442 | case 'number': 443 | val = val === 0 && (1/val) === -Infinity // `-0` 444 | ? '-0' 445 | : val.toString(); 446 | break; 447 | case 'date': 448 | val = '[Date: ' + val.toISOString() + ']'; 449 | break; 450 | case 'buffer': 451 | var json = val.toJSON(); 452 | // Based on the toJSON result 453 | json = json.data && json.type ? json.data : json; 454 | val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']'; 455 | break; 456 | default: 457 | val = (val == '[Function]' || val == '[Circular]') 458 | ? val 459 | : '"' + val + '"'; //string 460 | } 461 | return val; 462 | } 463 | 464 | for(var i in object) { 465 | if(!Object.prototype.hasOwnProperty.call(object, i)) continue; // not my business 466 | --length; 467 | str += '\n ' + repeat(' ', space) 468 | + (isArray(object) ? '' : '"' + i + '": ') // key 469 | + _stringify(object[i]) // value 470 | + (length ? ',' : ''); // comma 471 | } 472 | 473 | return str + (str.length != 1 // [], {} 474 | ? '\n' + repeat(' ', --space) + end 475 | : end); 476 | } 477 | 478 | /** 479 | * Return if obj is a Buffer 480 | * @param {Object} arg 481 | * @return {Boolean} 482 | * @api private 483 | */ 484 | exports.isBuffer = function (arg) { 485 | return typeof Buffer !== 'undefined' && Buffer.isBuffer(arg); 486 | }; 487 | 488 | /** 489 | * @summary Return a new Thing that has the keys in sorted order. Recursive. 490 | * @description If the Thing... 491 | * - has already been seen, return string `'[Circular]'` 492 | * - is `undefined`, return string `'[undefined]'` 493 | * - is `null`, return value `null` 494 | * - is some other primitive, return the value 495 | * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method 496 | * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again. 497 | * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()` 498 | * 499 | * @param {*} value Thing to inspect. May or may not have properties. 500 | * @param {Array} [stack=[]] Stack of seen values 501 | * @return {(Object|Array|Function|string|undefined)} 502 | * @see {@link exports.stringify} 503 | * @api private 504 | */ 505 | 506 | exports.canonicalize = function(value, stack) { 507 | var canonicalizedObj, 508 | type = exports.type(value), 509 | prop, 510 | withStack = function withStack(value, fn) { 511 | stack.push(value); 512 | fn(); 513 | stack.pop(); 514 | }; 515 | 516 | stack = stack || []; 517 | 518 | if (exports.indexOf(stack, value) !== -1) { 519 | return '[Circular]'; 520 | } 521 | 522 | switch(type) { 523 | case 'undefined': 524 | case 'buffer': 525 | case 'null': 526 | canonicalizedObj = value; 527 | break; 528 | case 'array': 529 | withStack(value, function () { 530 | canonicalizedObj = exports.map(value, function (item) { 531 | return exports.canonicalize(item, stack); 532 | }); 533 | }); 534 | break; 535 | case 'function': 536 | for (prop in value) { 537 | canonicalizedObj = {}; 538 | break; 539 | } 540 | if (!canonicalizedObj) { 541 | canonicalizedObj = emptyRepresentation(value, type); 542 | break; 543 | } 544 | /* falls through */ 545 | case 'object': 546 | canonicalizedObj = canonicalizedObj || {}; 547 | withStack(value, function () { 548 | exports.forEach(exports.keys(value).sort(), function (key) { 549 | canonicalizedObj[key] = exports.canonicalize(value[key], stack); 550 | }); 551 | }); 552 | break; 553 | case 'date': 554 | case 'number': 555 | case 'regexp': 556 | case 'boolean': 557 | canonicalizedObj = value; 558 | break; 559 | default: 560 | canonicalizedObj = value.toString(); 561 | } 562 | 563 | return canonicalizedObj; 564 | }; 565 | 566 | /** 567 | * Lookup file names at the given `path`. 568 | */ 569 | exports.lookupFiles = function lookupFiles(path, extensions, recursive) { 570 | var files = []; 571 | var re = new RegExp('\\.(' + extensions.join('|') + ')$'); 572 | 573 | if (!exists(path)) { 574 | if (exists(path + '.js')) { 575 | path += '.js'; 576 | } else { 577 | files = glob.sync(path); 578 | if (!files.length) throw new Error("cannot resolve path (or pattern) '" + path + "'"); 579 | return files; 580 | } 581 | } 582 | 583 | try { 584 | var stat = fs.statSync(path); 585 | if (stat.isFile()) return path; 586 | } 587 | catch (ignored) { 588 | return; 589 | } 590 | 591 | fs.readdirSync(path).forEach(function(file) { 592 | file = join(path, file); 593 | try { 594 | var stat = fs.statSync(file); 595 | if (stat.isDirectory()) { 596 | if (recursive) { 597 | files = files.concat(lookupFiles(file, extensions, recursive)); 598 | } 599 | return; 600 | } 601 | } 602 | catch (ignored) { 603 | return; 604 | } 605 | if (!stat.isFile() || !re.test(file) || basename(file)[0] === '.') return; 606 | files.push(file); 607 | }); 608 | 609 | return files; 610 | }; 611 | 612 | /** 613 | * Generate an undefined error with a message warning the user. 614 | * 615 | * @return {Error} 616 | */ 617 | 618 | exports.undefinedError = function() { 619 | return new Error('Caught undefined error, did you throw without specifying what?'); 620 | }; 621 | 622 | /** 623 | * Generate an undefined error if `err` is not defined. 624 | * 625 | * @param {Error} err 626 | * @return {Error} 627 | */ 628 | 629 | exports.getError = function(err) { 630 | return err || exports.undefinedError(); 631 | }; 632 | 633 | 634 | /** 635 | * @summary 636 | * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) 637 | * @description 638 | * When invoking this function you get a filter function that get the Error.stack as an input, 639 | * and return a prettify output. 640 | * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace). 641 | * @returns {Function} 642 | */ 643 | 644 | exports.stackTraceFilter = function() { 645 | var slash = '/' 646 | , is = typeof document === 'undefined' 647 | ? { node: true } 648 | : { browser: true } 649 | , cwd = is.node 650 | ? process.cwd() + slash 651 | : location.href.replace(/\/[^\/]*$/, '/'); 652 | 653 | function isNodeModule (line) { 654 | return (~line.indexOf('node_modules')); 655 | } 656 | 657 | function isMochaInternal (line) { 658 | return (~line.indexOf('node_modules' + slash + 'tap-mocha-reporter')) || 659 | (~line.indexOf('components' + slash + 'mochajs')) || 660 | (~line.indexOf('components' + slash + 'mocha')); 661 | } 662 | 663 | // node_modules, bower, componentJS 664 | function isBrowserModule(line) { 665 | return (~line.indexOf('node_modules')) || 666 | (~line.indexOf('components')); 667 | } 668 | 669 | function isNodeInternal (line) { 670 | return (~line.indexOf('(timers.js:')) || 671 | (~line.indexOf('(domain.js:')) || 672 | (~line.indexOf('(events.js:')) || 673 | (~line.indexOf('(node.js:')) || 674 | (~line.indexOf('(module.js:')) || 675 | (~line.indexOf('at node.js:')) || 676 | (~line.indexOf('GeneratorFunctionPrototype.next (native)')) || 677 | false 678 | } 679 | 680 | return function(stack) { 681 | stack = stack.split('\n'); 682 | 683 | stack = stack.reduce(function (list, line) { 684 | if (is.node && (isNodeModule(line) || 685 | isMochaInternal(line) || 686 | isNodeInternal(line))) 687 | return list; 688 | 689 | if (is.browser && (isBrowserModule(line))) 690 | return list; 691 | 692 | // Clean up cwd(absolute) 693 | list.push(line.replace(cwd, '')); 694 | return list; 695 | }, []); 696 | 697 | return stack.join('\n'); 698 | } 699 | }; 700 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tap-mocha-reporter", 3 | "version": "5.0.4", 4 | "description": "Format a TAP stream using Mocha's set of reporters", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap test/*.js", 8 | "preversion": "npm test", 9 | "postversion": "npm publish", 10 | "postpublish": "git push origin --follow-tags" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/isaacs/tap-mocha-reporter" 15 | }, 16 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/isaacs/tap-mocha-reporter/issues" 20 | }, 21 | "homepage": "https://github.com/isaacs/tap-mocha-reporter", 22 | "dependencies": { 23 | "color-support": "^1.1.0", 24 | "debug": "^4.1.1", 25 | "diff": "^4.0.1", 26 | "escape-string-regexp": "^2.0.0", 27 | "glob": "^7.0.5", 28 | "tap-parser": "^11.0.0", 29 | "tap-yaml": "^1.0.0", 30 | "unicode-length": "^2.0.2" 31 | }, 32 | "devDependencies": { 33 | "tap": "^15.2.2" 34 | }, 35 | "bin": "index.js", 36 | "files": [ 37 | "index.js", 38 | "lib" 39 | ], 40 | "engines": { 41 | "node": ">= 8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var tap = require('tap') 2 | tap.pass('this is a test') 3 | --------------------------------------------------------------------------------