/** * Module dependencies. */ var fs = require('fs') , path = require('path') , basename = path.basename , exists = fs.existsSync || path.existsSync , glob = require('glob') , join = path.join , debug = require('debug')('mocha:watch'); /** * Ignored directories. */ var ignore = ['node_modules', '.git']; /** * Escape special characters in the given string of html. * * @param {String} html * @return {String} * @api private */ exports.escape = function(html){ return String(html) .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); }; /** * Array#forEach (<=IE8) * * @param {Array} array * @param {Function} fn * @param {Object} scope * @api private */ exports.forEach = function(arr, fn, scope){ for (var i = 0, l = arr.length; i < l; i++) fn.call(scope, arr[i], i); }; /** * Array#map (<=IE8) * * @param {Array} array * @param {Function} fn * @param {Object} scope * @api private */ 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) * * @parma {Array} arr * @param {Object} obj to find index of * @param {Number} start * @api private */ 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) * * @param {Array} array * @param {Function} fn * @param {Object} initial value * @api private */ 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) * * @param {Array} array * @param {Function} fn * @api private */ 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) * * @param {Object} obj * @return {Array} keys * @api private */ exports.keys = Object.keys || function(obj) { var keys = [] , 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. * * @param {Array} files * @param {Function} fn * @api private */ exports.watch = function(files, fn){ var options = { interval: 100 }; files.forEach(function(file){ debug('file %s', file); fs.watchFile(file, options, function(curr, prev){ if (prev.mtime < curr.mtime) fn(file); }); }); }; /** * Array.isArray (<=IE8) * * @param {Object} obj * @return {Boolean} * @api private */ var isArray = Array.isArray || function (obj) { return '[object Array]' == {}.toString.call(obj); }; /** * @description * 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. */ function ignored(path){ return !~ignore.indexOf(path); } /** * Lookup files in the given `dir`. * * @return {Array} * @api private */ exports.files = function(dir, ext, ret){ ret = ret || []; ext = ext || ['js']; var re = new RegExp('\\.(' + ext.join('|') + ')$'); fs.readdirSync(dir) .filter(ignored) .forEach(function(path){ path = join(dir, path); if (fs.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`. * * @param {String} str * @return {String} * @api private */ exports.slug = function(str){ return str .toLowerCase() .replace(/ +/g, '-') .replace(/[^-\w]/g, ''); }; /** * Strip the function definition from `str`, * and re-indent for pre whitespace. */ 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 , tabs = str.match(/^\n?(\t*)/)[1].length , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm'); str = str.replace(re, ''); return exports.trim(str); }; /** * Trim the given `str`. * * @param {String} str * @return {String} * @api private */ exports.trim = function(str){ return str.replace(/^\s+|\s+$/g, ''); }; /** * Parse the given `qs`. * * @param {String} qs * @return {Object} * @api private */ exports.parseQuery = function(qs){ return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){ var i = pair.indexOf('=') , key = pair.slice(0, i) , val = pair.slice(++i); obj[key] = decodeURIComponent(val); return obj; }, {}); }; /** * Highlight the given string of `js`. * * @param {String} js * @return {String} * @api private */ function highlight(js) { return js .replace(//g, '>') .replace(/\/\/(.*)/gm, '//$1') .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`. * * @param {String} name * @api private */ 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()` * * @param {*} value Value to inspect * @param {string} [type] The type of the value, if known. * @returns {string} */ var emptyRepresentation = 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 `{}.toString()` what it thinks it is. * @param {*} value Anything * @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' * @api private * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString * @returns {string} */ exports.type = function type(value) { if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { return 'buffer'; } return Object.prototype.toString.call(value) .replace(/^\[.+\s(.+?)\]$/, '$1') .toLowerCase(); }; /** * @summary Stringify `value`. * @description 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(). * * @see exports.type * @param {*} value * @return {string} * @api private */ 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); }; /** * @description * like JSON.stringify but more sense. * @param {Object} object * @param {Number=} spaces * @param {number=} depth * @returns {*} * @private */ function jsonStringify(object, spaces, depth) { if(typeof spaces == 'undefined') return _stringify(object); // primitive types depth = depth || 1; var space = spaces * depth , str = isArray(object) ? '[' : '{' , end = isArray(object) ? ']' : '}' , length = object.length || exports.keys(object).length , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill 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 'number': val = val === 0 && (1/val) === -Infinity // `-0` ? '-0' : val.toString(); break; case 'date': val = '[Date: ' + val.toISOString() + ']'; 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.hasOwnProperty(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); } /** * Return if obj is a Buffer * @param {Object} arg * @return {Boolean} * @api private */ exports.isBuffer = function (arg) { return typeof Buffer !== 'undefined' && Buffer.isBuffer(arg); }; /** * @summary Return a new Thing that has the keys in sorted order. Recursive. * @description 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()` * * @param {*} value Thing to inspect. May or may not have properties. * @param {Array} [stack=[]] Stack of seen values * @return {(Object|Array|Function|string|undefined)} * @see {@link exports.stringify} * @api private */ exports.canonicalize = function(value, stack) { var canonicalizedObj, type = exports.type(value), prop, withStack = 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': for (prop in value) { canonicalizedObj = {}; break; } 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': canonicalizedObj = value; break; default: canonicalizedObj = value.toString(); } return canonicalizedObj; }; /** * Lookup file names at the given `path`. */ 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 = fs.statSync(path); if (stat.isFile()) return path; } catch (ignored) { return; } fs.readdirSync(path).forEach(function(file) { file = join(path, file); try { var stat = fs.statSync(file); if (stat.isDirectory()) { if (recursive) { files = files.concat(lookupFiles(file, extensions, recursive)); } return; } } catch (ignored) { 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, node_modules, bower and componentJS from stack trace). * @returns {Function} */ exports.stackTraceFilter = function() { var slash = '/' , is = typeof document === 'undefined' ? { node: true } : { browser: true } , cwd = is.node ? process.cwd() + slash : location.href.replace(/\/[^\/]*$/, '/'); function isNodeModule (line) { return (~line.indexOf('node_modules')); } function isMochaInternal (line) { return (~line.indexOf('node_modules' + slash + 'mocha')) || (~line.indexOf('components' + slash + 'mochajs')) || (~line.indexOf('components' + slash + 'mocha')); } // node_modules, bower, componentJS function isBrowserModule(line) { return (~line.indexOf('node_modules')) || (~line.indexOf('components')); } 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 (is.node && (isNodeModule(line) || isMochaInternal(line) || isNodeInternal(line))) return list; if (is.browser && (isBrowserModule(line))) return list; // Clean up cwd(absolute) list.push(line.replace(cwd, '')); return list; }, []); return stack.join('\n'); } };