'use strict'; /** * Various utility functions used throughout Mocha's codebase. * @module utils */ /** * Module dependencies. */ var fs = require('fs'); var path = require('path'); var util = require('util'); var glob = require('glob'); var he = require('he'); var errors = require('./errors'); var createNoFilesMatchPatternError = errors.createNoFilesMatchPatternError; var createMissingArgumentError = errors.createMissingArgumentError; var assign = (exports.assign = require('object.assign').getPolyfill()); /** * Inherit the prototype methods from one constructor into another. * * @param {function} ctor - Constructor function which needs to inherit the * prototype. * @param {function} superCtor - Constructor function to inherit prototype from. * @throws {TypeError} if either constructor is null, or if super constructor * lacks a prototype. */ exports.inherits = util.inherits; /** * Escape special characters in the given string of html. * * @private * @param {string} html * @return {string} */ exports.escape = function(html) { return he.encode(String(html), {useNamedReferences: false}); }; /** * Test if the given obj is type of string. * * @private * @param {Object} obj * @return {boolean} */ exports.isString = function(obj) { return typeof obj === 'string'; }; /** * Watch the given `files` for changes * and invoke `fn(file)` on modification. * * @private * @param {Array} files * @param {Function} fn */ exports.watch = function(files, fn) { var options = {interval: 100}; var debug = require('debug')('mocha:watch'); files.forEach(function(file) { debug('file %s', file); fs.watchFile(file, options, function(curr, prev) { if (prev.mtime < curr.mtime) { fn(file); } }); }); }; /** * Predicate to screen `pathname` for further consideration. * * @description * Returns false for pathname referencing: * * * @private * @param {string} pathname - File or directory name to screen * @return {boolean} whether pathname should be further considered * @example * ['node_modules', 'test.js'].filter(considerFurther); // => ['test.js'] */ function considerFurther(pathname) { var ignore = ['node_modules', '.git']; return !~ignore.indexOf(pathname); } /** * Lookup files in the given `dir`. * * @description * Filenames are returned in _traversal_ order by the OS/filesystem. * **Make no assumption that the names will be sorted in any fashion.** * * @private * @param {string} dir * @param {string[]} [exts=['js']] * @param {Array} [ret=[]] * @return {Array} */ exports.files = function(dir, exts, ret) { ret = ret || []; exts = exts || ['js']; fs.readdirSync(dir) .filter(considerFurther) .forEach(function(dirent) { var pathname = path.join(dir, dirent); if (fs.lstatSync(pathname).isDirectory()) { exports.files(pathname, exts, ret); } else if (hasMatchingExtname(pathname, exts)) { ret.push(pathname); } }); return ret; }; /** * Compute a slug from the given `str`. * * @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/, '') // (traditional)-> space/name parameters body (lambda)-> parameters body multi-statement/single keep body content .replace( /^function(?:\s*|\s+[^(]*)\([^)]*\)\s*\{((?:.|\n)*?)\s*\}$|^\([^)]*\)\s*=>\s*(?:\{((?:.|\n)*?)\s*\}|((?:.|\n)*))$/, '$1$2$3' ); var spaces = str.match(/^\n?( *)/)[1].length; var tabs = str.match(/^\n?(\t*)/)[1].length; var re = new RegExp( '^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs || spaces) + '}', 'gm' ); str = str.replace(re, ''); return str.trim(); }; /** * Parse the given `qs`. * * @private * @param {string} qs * @return {Object} */ exports.parseQuery = function(qs) { return qs .replace('?', '') .split('&') .reduce(function(obj, pair) { var i = pair.indexOf('='); var key = pair.slice(0, i); var val = pair.slice(++i); // Due to how the URLSearchParams API treats spaces obj[key] = decodeURIComponent(val.replace(/\+/g, '%20')); return obj; }, {}); }; /** * Highlight the given string of `js`. * * @private * @param {string} js * @return {string} */ 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`. * * @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()` * * @private * @param {*} value The value to inspect. * @param {string} typeHint The type of the value * @returns {string} */ function emptyRepresentation(value, typeHint) { switch (typeHint) { 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. * * @private * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString * @param {*} value The value to test. * @returns {string} Computed type * @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' * type(new String('foo') // 'object' */ var type = (exports.type = function type(value) { if (value === undefined) { return 'undefined'; } else if (value === null) { return 'null'; } else if (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(). * * @private * @see exports.type * @param {*} value * @return {string} */ exports.stringify = function(value) { var typeHint = type(value); if (!~['object', 'array', 'function'].indexOf(typeHint)) { if (typeHint === 'buffer') { var json = Buffer.prototype.toJSON.call(value); // Based on the toJSON result return jsonStringify( json.data && json.type ? json.data : json, 2 ).replace(/,(\n|$)/g, '$1'); } // IE7/IE8 has a bizarre String constructor; needs to be coerced // into an array and back to obj. if (typeHint === 'string' && typeof value === 'object') { value = value.split('').reduce(function(acc, char, idx) { acc[idx] = char; return acc; }, {}); typeHint = 'object'; } else { return jsonStringify(value); } } for (var prop in value) { if (Object.prototype.hasOwnProperty.call(value, prop)) { return jsonStringify( exports.canonicalize(value, null, typeHint), 2 ).replace(/,(\n|$)/g, '$1'); } } return emptyRepresentation(value, typeHint); }; /** * like JSON.stringify but more sense. * * @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 = Array.isArray(object) ? '[' : '{'; var end = Array.isArray(object) ? ']' : '}'; var length = typeof object.length === 'number' ? object.length : Object.keys(object).length; // `.repeat()` polyfill function repeat(s, n) { return new Array(n).join(s); } function _stringify(val) { switch (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 = isNaN(val.getTime()) ? val.toString() : val.toISOString(); 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) + (Array.isArray(object) ? '' : '"' + i + '": ') + // key _stringify(object[i]) + // value (length ? ',' : ''); // comma } return ( str + // [], {} (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end) ); } /** * 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()` * * @private * @see {@link exports.stringify} * @param {*} value Thing to inspect. May or may not have properties. * @param {Array} [stack=[]] Stack of seen values * @param {string} [typeHint] Type hint * @return {(Object|Array|Function|string|undefined)} */ exports.canonicalize = function canonicalize(value, stack, typeHint) { var canonicalizedObj; /* eslint-disable no-unused-vars */ var prop; /* eslint-enable no-unused-vars */ typeHint = typeHint || type(value); function withStack(value, fn) { stack.push(value); fn(); stack.pop(); } stack = stack || []; if (stack.indexOf(value) !== -1) { return '[Circular]'; } switch (typeHint) { case 'undefined': case 'buffer': case 'null': canonicalizedObj = value; break; case 'array': withStack(value, function() { canonicalizedObj = value.map(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, typeHint); break; } /* falls through */ case 'object': canonicalizedObj = canonicalizedObj || {}; withStack(value, function() { Object.keys(value) .sort() .forEach(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; }; /** * Determines if pathname has a matching file extension. * * @private * @param {string} pathname - Pathname to check for match. * @param {string[]} exts - List of file extensions (sans period). * @return {boolean} whether file extension matches. * @example * hasMatchingExtname('foo.html', ['js', 'css']); // => false */ function hasMatchingExtname(pathname, exts) { var suffix = path.extname(pathname).slice(1); return exts.some(function(element) { return suffix === element; }); } /** * Determines if pathname would be a "hidden" file (or directory) on UN*X. * * @description * On UN*X, pathnames beginning with a full stop (aka dot) are hidden during * typical usage. Dotfiles, plain-text configuration files, are prime examples. * * @see {@link http://xahlee.info/UnixResource_dir/writ/unix_origin_of_dot_filename.html|Origin of Dot File Names} * * @private * @param {string} pathname - Pathname to check for match. * @return {boolean} whether pathname would be considered a hidden file. * @example * isHiddenOnUnix('.profile'); // => true */ function isHiddenOnUnix(pathname) { return path.basename(pathname)[0] === '.'; } /** * Lookup file names at the given `path`. * * @description * Filenames are returned in _traversal_ order by the OS/filesystem. * **Make no assumption that the names will be sorted in any fashion.** * * @public * @memberof Mocha.utils * @todo Fix extension handling * @param {string} filepath - Base path to start searching from. * @param {string[]} extensions - File extensions to look for. * @param {boolean} recursive - Whether to recurse into subdirectories. * @return {string[]} An array of paths. * @throws {Error} if no files match pattern. * @throws {TypeError} if `filepath` is directory and `extensions` not provided. */ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { var files = []; var stat; if (!fs.existsSync(filepath)) { if (fs.existsSync(filepath + '.js')) { filepath += '.js'; } else { // Handle glob files = glob.sync(filepath); if (!files.length) { throw createNoFilesMatchPatternError( 'Cannot find any files matching pattern ' + exports.dQuote(filepath), filepath ); } return files; } } // Handle file try { stat = fs.statSync(filepath); if (stat.isFile()) { return filepath; } } catch (err) { // ignore error return; } // Handle directory fs.readdirSync(filepath).forEach(function(dirent) { var pathname = path.join(filepath, dirent); var stat; try { stat = fs.statSync(pathname); if (stat.isDirectory()) { if (recursive) { files = files.concat(lookupFiles(pathname, extensions, recursive)); } return; } } catch (err) { // ignore error return; } if (!extensions) { throw createMissingArgumentError( util.format( 'Argument %s required when argument %s is a directory', exports.sQuote('extensions'), exports.sQuote('filepath') ), 'extensions', 'array' ); } if ( !stat.isFile() || !hasMatchingExtname(pathname, extensions) || isHiddenOnUnix(pathname) ) { return; } files.push(pathname); }); return files; }; /** * process.emitWarning or a polyfill * @see https://nodejs.org/api/process.html#process_process_emitwarning_warning_options * @ignore */ function emitWarning(msg, type) { if (process.emitWarning) { process.emitWarning(msg, type); } else { process.nextTick(function() { console.warn(type + ': ' + msg); }); } } /** * Show a deprecation warning. Each distinct message is only displayed once. * Ignores empty messages. * * @param {string} [msg] - Warning to print * @private */ exports.deprecate = function deprecate(msg) { msg = String(msg); if (msg && !deprecate.cache[msg]) { deprecate.cache[msg] = true; emitWarning(msg, 'DeprecationWarning'); } }; exports.deprecate.cache = {}; /** * Show a generic warning. * Ignores empty messages. * * @param {string} [msg] - Warning to print * @private */ exports.warn = function warn(msg) { if (msg) { emitWarning(msg); } }; /** * @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 is = typeof document === 'undefined' ? {node: true} : {browser: true}; var slash = path.sep; var cwd; if (is.node) { cwd = process.cwd() + slash; } else { cwd = (typeof location === 'undefined' ? window.location : location ).href.replace(/\/[^/]*$/, '/'); slash = '/'; } function isMochaInternal(line) { return ( ~line.indexOf('node_modules' + 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 = stack.reduce(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'); }; }; /** * Crude, but effective. * @public * @param {*} value * @returns {boolean} Whether or not `value` is a Promise */ exports.isPromise = function isPromise(value) { return ( typeof value === 'object' && value !== null && typeof value.then === 'function' ); }; /** * Clamps a numeric value to an inclusive range. * * @param {number} value - Value to be clamped. * @param {numer[]} range - Two element array specifying [min, max] range. * @returns {number} clamped value */ exports.clamp = function clamp(value, range) { return Math.min(Math.max(value, range[0]), range[1]); }; /** * Single quote text by combining with undirectional ASCII quotation marks. * * @description * Provides a simple means of markup for quoting text to be used in output. * Use this to quote names of variables, methods, and packages. * * package 'foo' cannot be found * * @private * @param {string} str - Value to be quoted. * @returns {string} quoted value * @example * sQuote('n') // => 'n' */ exports.sQuote = function(str) { return "'" + str + "'"; }; /** * Double quote text by combining with undirectional ASCII quotation marks. * * @description * Provides a simple means of markup for quoting text to be used in output. * Use this to quote names of datatypes, classes, pathnames, and strings. * * argument 'value' must be "string" or "number" * * @private * @param {string} str - Value to be quoted. * @returns {string} quoted value * @example * dQuote('number') // => "number" */ exports.dQuote = function(str) { return '"' + str + '"'; }; /** * Provides simplistic message translation for dealing with plurality. * * @description * Use this to create messages which need to be singular or plural. * Some languages have several plural forms, so _complete_ message clauses * are preferable to generating the message on the fly. * * @private * @param {number} n - Non-negative integer * @param {string} msg1 - Message to be used in English for `n = 1` * @param {string} msg2 - Message to be used in English for `n = 0, 2, 3, ...` * @returns {string} message corresponding to value of `n` * @example * var sprintf = require('util').format; * var pkgs = ['one', 'two']; * var msg = sprintf( * ngettext( * pkgs.length, * 'cannot load package: %s', * 'cannot load packages: %s' * ), * pkgs.map(sQuote).join(', ') * ); * console.log(msg); // => cannot load packages: 'one', 'two' */ exports.ngettext = function(n, msg1, msg2) { if (typeof n === 'number' && n >= 0) { return n === 1 ? msg1 : msg2; } }; /** * It's a noop. * @public */ exports.noop = function() {}; /** * Creates a map-like object. * * @description * A "map" is an object with no prototype, for our purposes. In some cases * this would be more appropriate than a `Map`, especially if your environment * doesn't support it. Recommended for use in Mocha's public APIs. * * @public * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map|MDN:Map} * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Custom_and_Null_objects|MDN:Object.create - Custom objects} * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign|MDN:Object.assign} * @param {...*} [obj] - Arguments to `Object.assign()`. * @returns {Object} An object with no prototype, having `...obj` properties */ exports.createMap = function(obj) { return assign.apply( null, [Object.create(null)].concat(Array.prototype.slice.call(arguments)) ); }; /** * Creates a read-only map-like object. * * @description * This differs from {@link module:utils.createMap createMap} only in that * the argument must be non-empty, because the result is frozen. * * @see {@link module:utils.createMap createMap} * @param {...*} [obj] - Arguments to `Object.assign()`. * @returns {Object} A frozen object with no prototype, having `...obj` properties * @throws {TypeError} if argument is not a non-empty object. */ exports.defineConstants = function(obj) { if (type(obj) !== 'object' || !Object.keys(obj).length) { throw new TypeError('Invalid argument; expected a non-empty object'); } return Object.freeze(exports.createMap(obj)); };