/* eslint-env browser */
/**
* Module dependencies.
*/
var basename = require('path').basename;
var debug = require('debug')('mocha:watch');
var exists = require('fs').existsSync || require('path').existsSync;
var glob = require('glob');
var join = require('path').join;
var readdirSync = require('fs').readdirSync;
var statSync = require('fs').statSync;
var watchFile = require('fs').watchFile;
var toISOString = require('to-iso-string');
/**
* Ignored directories.
*/
var ignore = ['node_modules', '.git'];
exports.inherits = require('util').inherits;
/**
* Escape special characters in the given string of html.
*
* @api private
* @param {string} html
* @return {string}
*/
exports.escape = function(html) {
return String(html)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>');
};
/**
* Array#forEach (<=IE8)
*
* @api private
* @param {Array} arr
* @param {Function} fn
* @param {Object} scope
*/
exports.forEach = function(arr, fn, scope) {
for (var i = 0, l = arr.length; i < l; i++) {
fn.call(scope, arr[i], i);
}
};
/**
* Test if the given obj is type of string.
*
* @api private
* @param {Object} obj
* @return {boolean}
*/
exports.isString = function(obj) {
return typeof obj === 'string';
};
/**
* Array#map (<=IE8)
*
* @api private
* @param {Array} arr
* @param {Function} fn
* @param {Object} scope
* @return {Array}
*/
exports.map = function(arr, fn, scope) {
var result = [];
for (var i = 0, l = arr.length; i < l; i++) {
result.push(fn.call(scope, arr[i], i, arr));
}
return result;
};
/**
* Array#indexOf (<=IE8)
*
* @api private
* @param {Array} arr
* @param {Object} obj to find index of
* @param {number} start
* @return {number}
*/
exports.indexOf = function(arr, obj, start) {
for (var i = start || 0, l = arr.length; i < l; i++) {
if (arr[i] === obj) {
return i;
}
}
return -1;
};
/**
* Array#reduce (<=IE8)
*
* @api private
* @param {Array} arr
* @param {Function} fn
* @param {Object} val Initial value.
* @return {*}
*/
exports.reduce = function(arr, fn, val) {
var rval = val;
for (var i = 0, l = arr.length; i < l; i++) {
rval = fn(rval, arr[i], i, arr);
}
return rval;
};
/**
* Array#filter (<=IE8)
*
* @api private
* @param {Array} arr
* @param {Function} fn
* @return {Array}
*/
exports.filter = function(arr, fn) {
var ret = [];
for (var i = 0, l = arr.length; i < l; i++) {
var val = arr[i];
if (fn(val, i, arr)) {
ret.push(val);
}
}
return ret;
};
/**
* Object.keys (<=IE8)
*
* @api private
* @param {Object} obj
* @return {Array} keys
*/
exports.keys = typeof Object.keys === 'function' ? Object.keys : function(obj) {
var keys = [];
var has = Object.prototype.hasOwnProperty; // for `window` on <=IE8
for (var key in obj) {
if (has.call(obj, key)) {
keys.push(key);
}
}
return keys;
};
/**
* Watch the given `files` for changes
* and invoke `fn(file)` on modification.
*
* @api private
* @param {Array} files
* @param {Function} fn
*/
exports.watch = function(files, fn) {
var options = { interval: 100 };
files.forEach(function(file) {
debug('file %s', file);
watchFile(file, options, function(curr, prev) {
if (prev.mtime < curr.mtime) {
fn(file);
}
});
});
};
/**
* Array.isArray (<=IE8)
*
* @api private
* @param {Object} obj
* @return {Boolean}
*/
var isArray = typeof Array.isArray === 'function' ? Array.isArray : function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
exports.isArray = isArray;
/**
* Buffer.prototype.toJSON polyfill.
*
* @type {Function}
*/
if (typeof Buffer !== 'undefined' && Buffer.prototype) {
Buffer.prototype.toJSON = Buffer.prototype.toJSON || function() {
return Array.prototype.slice.call(this, 0);
};
}
/**
* Ignored files.
*
* @api private
* @param {string} path
* @return {boolean}
*/
function ignored(path) {
return !~ignore.indexOf(path);
}
/**
* Lookup files in the given `dir`.
*
* @api private
* @param {string} dir
* @param {string[]} [ext=['.js']]
* @param {Array} [ret=[]]
* @return {Array}
*/
exports.files = function(dir, ext, ret) {
ret = ret || [];
ext = ext || ['js'];
var re = new RegExp('\\.(' + ext.join('|') + ')$');
readdirSync(dir)
.filter(ignored)
.forEach(function(path) {
path = join(dir, path);
if (statSync(path).isDirectory()) {
exports.files(path, ext, ret);
} else if (path.match(re)) {
ret.push(path);
}
});
return ret;
};
/**
* Compute a slug from the given `str`.
*
* @api private
* @param {string} str
* @return {string}
*/
exports.slug = function(str) {
return str
.toLowerCase()
.replace(/ +/g, '-')
.replace(/[^-\w]/g, '');
};
/**
* Strip the function definition from `str`, and re-indent for pre whitespace.
*
* @param {string} str
* @return {string}
*/
exports.clean = function(str) {
str = str
.replace(/\r\n?|[\n\u2028\u2029]/g, '\n').replace(/^\uFEFF/, '')
.replace(/^function *\(.*\)\s*\{|\(.*\) *=> *\{?/, '')
.replace(/\s+\}$/, '');
var spaces = str.match(/^\n?( *)/)[1].length;
var tabs = str.match(/^\n?(\t*)/)[1].length;
var re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm');
str = str.replace(re, '');
return exports.trim(str);
};
/**
* Trim the given `str`.
*
* @api private
* @param {string} str
* @return {string}
*/
exports.trim = function(str) {
return str.replace(/^\s+|\s+$/g, '');
};
/**
* Parse the given `qs`.
*
* @api private
* @param {string} qs
* @return {Object}
*/
exports.parseQuery = function(qs) {
return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair) {
var i = pair.indexOf('=');
var key = pair.slice(0, i);
var val = pair.slice(++i);
obj[key] = decodeURIComponent(val);
return obj;
}, {});
};
/**
* Highlight the given string of `js`.
*
* @api private
* @param {string} js
* @return {string}
*/
function highlight(js) {
return js
.replace(//g, '>')
.replace(/\/\/(.*)/gm, '')
.replace(/('.*?')/gm, '$1')
.replace(/(\d+\.\d+)/gm, '$1')
.replace(/(\d+)/gm, '$1')
.replace(/\bnew[ \t]+(\w+)/gm, 'new $1')
.replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1');
}
/**
* Highlight the contents of tag `name`.
*
* @api private
* @param {string} name
*/
exports.highlightTags = function(name) {
var code = document.getElementById('mocha').getElementsByTagName(name);
for (var i = 0, len = code.length; i < len; ++i) {
code[i].innerHTML = highlight(code[i].innerHTML);
}
};
/**
* If a value could have properties, and has none, this function is called,
* which returns a string representation of the empty value.
*
* Functions w/ no properties return `'[Function]'`
* Arrays w/ length === 0 return `'[]'`
* Objects w/ no properties return `'{}'`
* All else: return result of `value.toString()`
*
* @api private
* @param {*} value The value to inspect.
* @param {string} [type] The type of the value, if known.
* @returns {string}
*/
function emptyRepresentation(value, type) {
type = type || exports.type(value);
switch (type) {
case 'function':
return '[Function]';
case 'object':
return '{}';
case 'array':
return '[]';
default:
return value.toString();
}
}
/**
* Takes some variable and asks `Object.prototype.toString()` what it thinks it
* is.
*
* @api private
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
* @param {*} value The value to test.
* @returns {string}
* @example
* type({}) // 'object'
* type([]) // 'array'
* type(1) // 'number'
* type(false) // 'boolean'
* type(Infinity) // 'number'
* type(null) // 'null'
* type(new Date()) // 'date'
* type(/foo/) // 'regexp'
* type('type') // 'string'
* type(global) // 'global'
*/
exports.type = function type(value) {
if (value === undefined) {
return 'undefined';
} else if (value === null) {
return 'null';
} else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
return 'buffer';
}
return Object.prototype.toString.call(value)
.replace(/^\[.+\s(.+?)\]$/, '$1')
.toLowerCase();
};
/**
* Stringify `value`. Different behavior depending on type of value:
*
* - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively.
* - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes.
* - If `value` is an *empty* object, function, or array, return result of function
* {@link emptyRepresentation}.
* - If `value` has properties, call {@link exports.canonicalize} on it, then return result of
* JSON.stringify().
*
* @api private
* @see exports.type
* @param {*} value
* @return {string}
*/
exports.stringify = function(value) {
var type = exports.type(value);
if (!~exports.indexOf(['object', 'array', 'function'], type)) {
if (type !== 'buffer') {
return jsonStringify(value);
}
var json = value.toJSON();
// Based on the toJSON result
return jsonStringify(json.data && json.type ? json.data : json, 2)
.replace(/,(\n|$)/g, '$1');
}
for (var prop in value) {
if (Object.prototype.hasOwnProperty.call(value, prop)) {
return jsonStringify(exports.canonicalize(value), 2).replace(/,(\n|$)/g, '$1');
}
}
return emptyRepresentation(value, type);
};
/**
* like JSON.stringify but more sense.
*
* @api private
* @param {Object} object
* @param {number=} spaces
* @param {number=} depth
* @returns {*}
*/
function jsonStringify(object, spaces, depth) {
if (typeof spaces === 'undefined') {
// primitive types
return _stringify(object);
}
depth = depth || 1;
var space = spaces * depth;
var str = isArray(object) ? '[' : '{';
var end = isArray(object) ? ']' : '}';
var length = typeof object.length === 'number' ? object.length : exports.keys(object).length;
// `.repeat()` polyfill
function repeat(s, n) {
return new Array(n).join(s);
}
function _stringify(val) {
switch (exports.type(val)) {
case 'null':
case 'undefined':
val = '[' + val + ']';
break;
case 'array':
case 'object':
val = jsonStringify(val, spaces, depth + 1);
break;
case 'boolean':
case 'regexp':
case 'symbol':
case 'number':
val = val === 0 && (1 / val) === -Infinity // `-0`
? '-0'
: val.toString();
break;
case 'date':
var sDate;
if (isNaN(val.getTime())) { // Invalid date
sDate = val.toString();
} else {
sDate = val.toISOString ? val.toISOString() : toISOString(val);
}
val = '[Date: ' + sDate + ']';
break;
case 'buffer':
var json = val.toJSON();
// Based on the toJSON result
json = json.data && json.type ? json.data : json;
val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']';
break;
default:
val = (val === '[Function]' || val === '[Circular]')
? val
: JSON.stringify(val); // string
}
return val;
}
for (var i in object) {
if (!Object.prototype.hasOwnProperty.call(object, i)) {
continue; // not my business
}
--length;
str += '\n ' + repeat(' ', space)
+ (isArray(object) ? '' : '"' + i + '": ') // key
+ _stringify(object[i]) // value
+ (length ? ',' : ''); // comma
}
return str
// [], {}
+ (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end);
}
/**
* Test if a value is a buffer.
*
* @api private
* @param {*} value The value to test.
* @return {boolean} True if `value` is a buffer, otherwise false
*/
exports.isBuffer = function(value) {
return typeof Buffer !== 'undefined' && Buffer.isBuffer(value);
};
/**
* Return a new Thing that has the keys in sorted order. Recursive.
*
* If the Thing...
* - has already been seen, return string `'[Circular]'`
* - is `undefined`, return string `'[undefined]'`
* - is `null`, return value `null`
* - is some other primitive, return the value
* - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method
* - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again.
* - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()`
*
* @api private
* @see {@link exports.stringify}
* @param {*} value Thing to inspect. May or may not have properties.
* @param {Array} [stack=[]] Stack of seen values
* @return {(Object|Array|Function|string|undefined)}
*/
exports.canonicalize = function(value, stack) {
var canonicalizedObj;
/* eslint-disable no-unused-vars */
var prop;
/* eslint-enable no-unused-vars */
var type = exports.type(value);
function withStack(value, fn) {
stack.push(value);
fn();
stack.pop();
}
stack = stack || [];
if (exports.indexOf(stack, value) !== -1) {
return '[Circular]';
}
switch (type) {
case 'undefined':
case 'buffer':
case 'null':
canonicalizedObj = value;
break;
case 'array':
withStack(value, function() {
canonicalizedObj = exports.map(value, function(item) {
return exports.canonicalize(item, stack);
});
});
break;
case 'function':
/* eslint-disable guard-for-in */
for (prop in value) {
canonicalizedObj = {};
break;
}
/* eslint-enable guard-for-in */
if (!canonicalizedObj) {
canonicalizedObj = emptyRepresentation(value, type);
break;
}
/* falls through */
case 'object':
canonicalizedObj = canonicalizedObj || {};
withStack(value, function() {
exports.forEach(exports.keys(value).sort(), function(key) {
canonicalizedObj[key] = exports.canonicalize(value[key], stack);
});
});
break;
case 'date':
case 'number':
case 'regexp':
case 'boolean':
case 'symbol':
canonicalizedObj = value;
break;
default:
canonicalizedObj = value + '';
}
return canonicalizedObj;
};
/**
* Lookup file names at the given `path`.
*
* @api public
* @param {string} path Base path to start searching from.
* @param {string[]} extensions File extensions to look for.
* @param {boolean} recursive Whether or not to recurse into subdirectories.
* @return {string[]} An array of paths.
*/
exports.lookupFiles = function lookupFiles(path, extensions, recursive) {
var files = [];
var re = new RegExp('\\.(' + extensions.join('|') + ')$');
if (!exists(path)) {
if (exists(path + '.js')) {
path += '.js';
} else {
files = glob.sync(path);
if (!files.length) {
throw new Error("cannot resolve path (or pattern) '" + path + "'");
}
return files;
}
}
try {
var stat = statSync(path);
if (stat.isFile()) {
return path;
}
} catch (err) {
// ignore error
return;
}
readdirSync(path).forEach(function(file) {
file = join(path, file);
try {
var stat = statSync(file);
if (stat.isDirectory()) {
if (recursive) {
files = files.concat(lookupFiles(file, extensions, recursive));
}
return;
}
} catch (err) {
// ignore error
return;
}
if (!stat.isFile() || !re.test(file) || basename(file)[0] === '.') {
return;
}
files.push(file);
});
return files;
};
/**
* Generate an undefined error with a message warning the user.
*
* @return {Error}
*/
exports.undefinedError = function() {
return new Error('Caught undefined error, did you throw without specifying what?');
};
/**
* Generate an undefined error if `err` is not defined.
*
* @param {Error} err
* @return {Error}
*/
exports.getError = function(err) {
return err || exports.undefinedError();
};
/**
* @summary
* This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`)
* @description
* When invoking this function you get a filter function that get the Error.stack as an input,
* and return a prettify output.
* (i.e: strip Mocha and internal node functions from stack trace).
* @returns {Function}
*/
exports.stackTraceFilter = function() {
// TODO: Replace with `process.browser`
var slash = '/';
var is = typeof document === 'undefined' ? { node: true } : { browser: true };
var cwd = is.node
? process.cwd() + slash
: (typeof location === 'undefined' ? window.location : location).href.replace(/\/[^\/]*$/, '/');
function isMochaInternal(line) {
return (~line.indexOf('node_modules' + slash + 'mocha' + slash))
|| (~line.indexOf('components' + slash + 'mochajs' + slash))
|| (~line.indexOf('components' + slash + 'mocha' + slash))
|| (~line.indexOf(slash + 'mocha.js'));
}
function isNodeInternal(line) {
return (~line.indexOf('(timers.js:'))
|| (~line.indexOf('(events.js:'))
|| (~line.indexOf('(node.js:'))
|| (~line.indexOf('(module.js:'))
|| (~line.indexOf('GeneratorFunctionPrototype.next (native)'))
|| false;
}
return function(stack) {
stack = stack.split('\n');
stack = exports.reduce(stack, function(list, line) {
if (isMochaInternal(line)) {
return list;
}
if (is.node && isNodeInternal(line)) {
return list;
}
// Clean up cwd(absolute)
if (/\(?.+:\d+:\d+\)?$/.test(line)) {
line = line.replace(cwd, '');
}
list.push(line);
return list;
}, []);
return stack.join('\n');
};
};