utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. 'use strict';
  2. /**
  3. * @module
  4. */
  5. /**
  6. * Module dependencies.
  7. */
  8. var debug = require('debug')('mocha:watch');
  9. var fs = require('fs');
  10. var glob = require('glob');
  11. var path = require('path');
  12. var join = path.join;
  13. var he = require('he');
  14. /**
  15. * Ignored directories.
  16. */
  17. var ignore = ['node_modules', '.git'];
  18. exports.inherits = require('util').inherits;
  19. /**
  20. * Escape special characters in the given string of html.
  21. *
  22. * @api private
  23. * @param {string} html
  24. * @return {string}
  25. */
  26. exports.escape = function(html) {
  27. return he.encode(String(html), {useNamedReferences: false});
  28. };
  29. /**
  30. * Test if the given obj is type of string.
  31. *
  32. * @api private
  33. * @param {Object} obj
  34. * @return {boolean}
  35. */
  36. exports.isString = function(obj) {
  37. return typeof obj === 'string';
  38. };
  39. /**
  40. * Watch the given `files` for changes
  41. * and invoke `fn(file)` on modification.
  42. *
  43. * @api private
  44. * @param {Array} files
  45. * @param {Function} fn
  46. */
  47. exports.watch = function(files, fn) {
  48. var options = {interval: 100};
  49. files.forEach(function(file) {
  50. debug('file %s', file);
  51. fs.watchFile(file, options, function(curr, prev) {
  52. if (prev.mtime < curr.mtime) {
  53. fn(file);
  54. }
  55. });
  56. });
  57. };
  58. /**
  59. * Ignored files.
  60. *
  61. * @api private
  62. * @param {string} path
  63. * @return {boolean}
  64. */
  65. function ignored(path) {
  66. return !~ignore.indexOf(path);
  67. }
  68. /**
  69. * Lookup files in the given `dir`.
  70. *
  71. * @api private
  72. * @param {string} dir
  73. * @param {string[]} [ext=['.js']]
  74. * @param {Array} [ret=[]]
  75. * @return {Array}
  76. */
  77. exports.files = function(dir, ext, ret) {
  78. ret = ret || [];
  79. ext = ext || ['js'];
  80. var re = new RegExp('\\.(' + ext.join('|') + ')$');
  81. fs
  82. .readdirSync(dir)
  83. .filter(ignored)
  84. .forEach(function(path) {
  85. path = join(dir, path);
  86. if (fs.lstatSync(path).isDirectory()) {
  87. exports.files(path, ext, ret);
  88. } else if (path.match(re)) {
  89. ret.push(path);
  90. }
  91. });
  92. return ret;
  93. };
  94. /**
  95. * Compute a slug from the given `str`.
  96. *
  97. * @api private
  98. * @param {string} str
  99. * @return {string}
  100. */
  101. exports.slug = function(str) {
  102. return str
  103. .toLowerCase()
  104. .replace(/ +/g, '-')
  105. .replace(/[^-\w]/g, '');
  106. };
  107. /**
  108. * Strip the function definition from `str`, and re-indent for pre whitespace.
  109. *
  110. * @param {string} str
  111. * @return {string}
  112. */
  113. exports.clean = function(str) {
  114. str = str
  115. .replace(/\r\n?|[\n\u2028\u2029]/g, '\n')
  116. .replace(/^\uFEFF/, '')
  117. // (traditional)-> space/name parameters body (lambda)-> parameters body multi-statement/single keep body content
  118. .replace(
  119. /^function(?:\s*|\s+[^(]*)\([^)]*\)\s*\{((?:.|\n)*?)\s*\}$|^\([^)]*\)\s*=>\s*(?:\{((?:.|\n)*?)\s*\}|((?:.|\n)*))$/,
  120. '$1$2$3'
  121. );
  122. var spaces = str.match(/^\n?( *)/)[1].length;
  123. var tabs = str.match(/^\n?(\t*)/)[1].length;
  124. var re = new RegExp(
  125. '^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs || spaces) + '}',
  126. 'gm'
  127. );
  128. str = str.replace(re, '');
  129. return str.trim();
  130. };
  131. /**
  132. * Parse the given `qs`.
  133. *
  134. * @api private
  135. * @param {string} qs
  136. * @return {Object}
  137. */
  138. exports.parseQuery = function(qs) {
  139. return qs
  140. .replace('?', '')
  141. .split('&')
  142. .reduce(function(obj, pair) {
  143. var i = pair.indexOf('=');
  144. var key = pair.slice(0, i);
  145. var val = pair.slice(++i);
  146. // Due to how the URLSearchParams API treats spaces
  147. obj[key] = decodeURIComponent(val.replace(/\+/g, '%20'));
  148. return obj;
  149. }, {});
  150. };
  151. /**
  152. * Highlight the given string of `js`.
  153. *
  154. * @api private
  155. * @param {string} js
  156. * @return {string}
  157. */
  158. function highlight(js) {
  159. return js
  160. .replace(/</g, '&lt;')
  161. .replace(/>/g, '&gt;')
  162. .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
  163. .replace(/('.*?')/gm, '<span class="string">$1</span>')
  164. .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
  165. .replace(/(\d+)/gm, '<span class="number">$1</span>')
  166. .replace(
  167. /\bnew[ \t]+(\w+)/gm,
  168. '<span class="keyword">new</span> <span class="init">$1</span>'
  169. )
  170. .replace(
  171. /\b(function|new|throw|return|var|if|else)\b/gm,
  172. '<span class="keyword">$1</span>'
  173. );
  174. }
  175. /**
  176. * Highlight the contents of tag `name`.
  177. *
  178. * @api private
  179. * @param {string} name
  180. */
  181. exports.highlightTags = function(name) {
  182. var code = document.getElementById('mocha').getElementsByTagName(name);
  183. for (var i = 0, len = code.length; i < len; ++i) {
  184. code[i].innerHTML = highlight(code[i].innerHTML);
  185. }
  186. };
  187. /**
  188. * If a value could have properties, and has none, this function is called,
  189. * which returns a string representation of the empty value.
  190. *
  191. * Functions w/ no properties return `'[Function]'`
  192. * Arrays w/ length === 0 return `'[]'`
  193. * Objects w/ no properties return `'{}'`
  194. * All else: return result of `value.toString()`
  195. *
  196. * @api private
  197. * @param {*} value The value to inspect.
  198. * @param {string} typeHint The type of the value
  199. * @returns {string}
  200. */
  201. function emptyRepresentation(value, typeHint) {
  202. switch (typeHint) {
  203. case 'function':
  204. return '[Function]';
  205. case 'object':
  206. return '{}';
  207. case 'array':
  208. return '[]';
  209. default:
  210. return value.toString();
  211. }
  212. }
  213. /**
  214. * Takes some variable and asks `Object.prototype.toString()` what it thinks it
  215. * is.
  216. *
  217. * @api private
  218. * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
  219. * @param {*} value The value to test.
  220. * @returns {string} Computed type
  221. * @example
  222. * type({}) // 'object'
  223. * type([]) // 'array'
  224. * type(1) // 'number'
  225. * type(false) // 'boolean'
  226. * type(Infinity) // 'number'
  227. * type(null) // 'null'
  228. * type(new Date()) // 'date'
  229. * type(/foo/) // 'regexp'
  230. * type('type') // 'string'
  231. * type(global) // 'global'
  232. * type(new String('foo') // 'object'
  233. */
  234. var type = (exports.type = function type(value) {
  235. if (value === undefined) {
  236. return 'undefined';
  237. } else if (value === null) {
  238. return 'null';
  239. } else if (Buffer.isBuffer(value)) {
  240. return 'buffer';
  241. }
  242. return Object.prototype.toString
  243. .call(value)
  244. .replace(/^\[.+\s(.+?)]$/, '$1')
  245. .toLowerCase();
  246. });
  247. /**
  248. * Stringify `value`. Different behavior depending on type of value:
  249. *
  250. * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively.
  251. * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes.
  252. * - If `value` is an *empty* object, function, or array, return result of function
  253. * {@link emptyRepresentation}.
  254. * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of
  255. * JSON.stringify().
  256. *
  257. * @api private
  258. * @see exports.type
  259. * @param {*} value
  260. * @return {string}
  261. */
  262. exports.stringify = function(value) {
  263. var typeHint = type(value);
  264. if (!~['object', 'array', 'function'].indexOf(typeHint)) {
  265. if (typeHint === 'buffer') {
  266. var json = Buffer.prototype.toJSON.call(value);
  267. // Based on the toJSON result
  268. return jsonStringify(
  269. json.data && json.type ? json.data : json,
  270. 2
  271. ).replace(/,(\n|$)/g, '$1');
  272. }
  273. // IE7/IE8 has a bizarre String constructor; needs to be coerced
  274. // into an array and back to obj.
  275. if (typeHint === 'string' && typeof value === 'object') {
  276. value = value.split('').reduce(function(acc, char, idx) {
  277. acc[idx] = char;
  278. return acc;
  279. }, {});
  280. typeHint = 'object';
  281. } else {
  282. return jsonStringify(value);
  283. }
  284. }
  285. for (var prop in value) {
  286. if (Object.prototype.hasOwnProperty.call(value, prop)) {
  287. return jsonStringify(
  288. exports.canonicalize(value, null, typeHint),
  289. 2
  290. ).replace(/,(\n|$)/g, '$1');
  291. }
  292. }
  293. return emptyRepresentation(value, typeHint);
  294. };
  295. /**
  296. * like JSON.stringify but more sense.
  297. *
  298. * @api private
  299. * @param {Object} object
  300. * @param {number=} spaces
  301. * @param {number=} depth
  302. * @returns {*}
  303. */
  304. function jsonStringify(object, spaces, depth) {
  305. if (typeof spaces === 'undefined') {
  306. // primitive types
  307. return _stringify(object);
  308. }
  309. depth = depth || 1;
  310. var space = spaces * depth;
  311. var str = Array.isArray(object) ? '[' : '{';
  312. var end = Array.isArray(object) ? ']' : '}';
  313. var length =
  314. typeof object.length === 'number'
  315. ? object.length
  316. : Object.keys(object).length;
  317. // `.repeat()` polyfill
  318. function repeat(s, n) {
  319. return new Array(n).join(s);
  320. }
  321. function _stringify(val) {
  322. switch (type(val)) {
  323. case 'null':
  324. case 'undefined':
  325. val = '[' + val + ']';
  326. break;
  327. case 'array':
  328. case 'object':
  329. val = jsonStringify(val, spaces, depth + 1);
  330. break;
  331. case 'boolean':
  332. case 'regexp':
  333. case 'symbol':
  334. case 'number':
  335. val =
  336. val === 0 && 1 / val === -Infinity // `-0`
  337. ? '-0'
  338. : val.toString();
  339. break;
  340. case 'date':
  341. var sDate = isNaN(val.getTime()) ? val.toString() : val.toISOString();
  342. val = '[Date: ' + sDate + ']';
  343. break;
  344. case 'buffer':
  345. var json = val.toJSON();
  346. // Based on the toJSON result
  347. json = json.data && json.type ? json.data : json;
  348. val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']';
  349. break;
  350. default:
  351. val =
  352. val === '[Function]' || val === '[Circular]'
  353. ? val
  354. : JSON.stringify(val); // string
  355. }
  356. return val;
  357. }
  358. for (var i in object) {
  359. if (!Object.prototype.hasOwnProperty.call(object, i)) {
  360. continue; // not my business
  361. }
  362. --length;
  363. str +=
  364. '\n ' +
  365. repeat(' ', space) +
  366. (Array.isArray(object) ? '' : '"' + i + '": ') + // key
  367. _stringify(object[i]) + // value
  368. (length ? ',' : ''); // comma
  369. }
  370. return (
  371. str +
  372. // [], {}
  373. (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end)
  374. );
  375. }
  376. /**
  377. * Return a new Thing that has the keys in sorted order. Recursive.
  378. *
  379. * If the Thing...
  380. * - has already been seen, return string `'[Circular]'`
  381. * - is `undefined`, return string `'[undefined]'`
  382. * - is `null`, return value `null`
  383. * - is some other primitive, return the value
  384. * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method
  385. * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again.
  386. * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()`
  387. *
  388. * @api private
  389. * @see {@link exports.stringify}
  390. * @param {*} value Thing to inspect. May or may not have properties.
  391. * @param {Array} [stack=[]] Stack of seen values
  392. * @param {string} [typeHint] Type hint
  393. * @return {(Object|Array|Function|string|undefined)}
  394. */
  395. exports.canonicalize = function canonicalize(value, stack, typeHint) {
  396. var canonicalizedObj;
  397. /* eslint-disable no-unused-vars */
  398. var prop;
  399. /* eslint-enable no-unused-vars */
  400. typeHint = typeHint || type(value);
  401. function withStack(value, fn) {
  402. stack.push(value);
  403. fn();
  404. stack.pop();
  405. }
  406. stack = stack || [];
  407. if (stack.indexOf(value) !== -1) {
  408. return '[Circular]';
  409. }
  410. switch (typeHint) {
  411. case 'undefined':
  412. case 'buffer':
  413. case 'null':
  414. canonicalizedObj = value;
  415. break;
  416. case 'array':
  417. withStack(value, function() {
  418. canonicalizedObj = value.map(function(item) {
  419. return exports.canonicalize(item, stack);
  420. });
  421. });
  422. break;
  423. case 'function':
  424. /* eslint-disable guard-for-in */
  425. for (prop in value) {
  426. canonicalizedObj = {};
  427. break;
  428. }
  429. /* eslint-enable guard-for-in */
  430. if (!canonicalizedObj) {
  431. canonicalizedObj = emptyRepresentation(value, typeHint);
  432. break;
  433. }
  434. /* falls through */
  435. case 'object':
  436. canonicalizedObj = canonicalizedObj || {};
  437. withStack(value, function() {
  438. Object.keys(value)
  439. .sort()
  440. .forEach(function(key) {
  441. canonicalizedObj[key] = exports.canonicalize(value[key], stack);
  442. });
  443. });
  444. break;
  445. case 'date':
  446. case 'number':
  447. case 'regexp':
  448. case 'boolean':
  449. case 'symbol':
  450. canonicalizedObj = value;
  451. break;
  452. default:
  453. canonicalizedObj = value + '';
  454. }
  455. return canonicalizedObj;
  456. };
  457. /**
  458. * Lookup file names at the given `path`.
  459. *
  460. * @memberof Mocha.utils
  461. * @public
  462. * @api public
  463. * @param {string} filepath Base path to start searching from.
  464. * @param {string[]} extensions File extensions to look for.
  465. * @param {boolean} recursive Whether or not to recurse into subdirectories.
  466. * @return {string[]} An array of paths.
  467. */
  468. exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
  469. var files = [];
  470. if (!fs.existsSync(filepath)) {
  471. if (fs.existsSync(filepath + '.js')) {
  472. filepath += '.js';
  473. } else {
  474. files = glob.sync(filepath);
  475. if (!files.length) {
  476. throw new Error("cannot resolve path (or pattern) '" + filepath + "'");
  477. }
  478. return files;
  479. }
  480. }
  481. try {
  482. var stat = fs.statSync(filepath);
  483. if (stat.isFile()) {
  484. return filepath;
  485. }
  486. } catch (err) {
  487. // ignore error
  488. return;
  489. }
  490. fs.readdirSync(filepath).forEach(function(file) {
  491. file = path.join(filepath, file);
  492. try {
  493. var stat = fs.statSync(file);
  494. if (stat.isDirectory()) {
  495. if (recursive) {
  496. files = files.concat(lookupFiles(file, extensions, recursive));
  497. }
  498. return;
  499. }
  500. } catch (err) {
  501. // ignore error
  502. return;
  503. }
  504. if (!extensions) {
  505. throw new Error(
  506. 'extensions parameter required when filepath is a directory'
  507. );
  508. }
  509. var re = new RegExp('\\.(?:' + extensions.join('|') + ')$');
  510. if (!stat.isFile() || !re.test(file) || path.basename(file)[0] === '.') {
  511. return;
  512. }
  513. files.push(file);
  514. });
  515. return files;
  516. };
  517. /**
  518. * Generate an undefined error with a message warning the user.
  519. *
  520. * @return {Error}
  521. */
  522. exports.undefinedError = function() {
  523. return new Error(
  524. 'Caught undefined error, did you throw without specifying what?'
  525. );
  526. };
  527. /**
  528. * Generate an undefined error if `err` is not defined.
  529. *
  530. * @param {Error} err
  531. * @return {Error}
  532. */
  533. exports.getError = function(err) {
  534. return err || exports.undefinedError();
  535. };
  536. /**
  537. * @summary
  538. * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`)
  539. * @description
  540. * When invoking this function you get a filter function that get the Error.stack as an input,
  541. * and return a prettify output.
  542. * (i.e: strip Mocha and internal node functions from stack trace).
  543. * @returns {Function}
  544. */
  545. exports.stackTraceFilter = function() {
  546. // TODO: Replace with `process.browser`
  547. var is = typeof document === 'undefined' ? {node: true} : {browser: true};
  548. var slash = path.sep;
  549. var cwd;
  550. if (is.node) {
  551. cwd = process.cwd() + slash;
  552. } else {
  553. cwd = (typeof location === 'undefined'
  554. ? window.location
  555. : location
  556. ).href.replace(/\/[^/]*$/, '/');
  557. slash = '/';
  558. }
  559. function isMochaInternal(line) {
  560. return (
  561. ~line.indexOf('node_modules' + slash + 'mocha' + slash) ||
  562. ~line.indexOf('node_modules' + slash + 'mocha.js') ||
  563. ~line.indexOf('bower_components' + slash + 'mocha.js') ||
  564. ~line.indexOf(slash + 'mocha.js')
  565. );
  566. }
  567. function isNodeInternal(line) {
  568. return (
  569. ~line.indexOf('(timers.js:') ||
  570. ~line.indexOf('(events.js:') ||
  571. ~line.indexOf('(node.js:') ||
  572. ~line.indexOf('(module.js:') ||
  573. ~line.indexOf('GeneratorFunctionPrototype.next (native)') ||
  574. false
  575. );
  576. }
  577. return function(stack) {
  578. stack = stack.split('\n');
  579. stack = stack.reduce(function(list, line) {
  580. if (isMochaInternal(line)) {
  581. return list;
  582. }
  583. if (is.node && isNodeInternal(line)) {
  584. return list;
  585. }
  586. // Clean up cwd(absolute)
  587. if (/\(?.+:\d+:\d+\)?$/.test(line)) {
  588. line = line.replace('(' + cwd, '(');
  589. }
  590. list.push(line);
  591. return list;
  592. }, []);
  593. return stack.join('\n');
  594. };
  595. };
  596. /**
  597. * Crude, but effective.
  598. * @api
  599. * @param {*} value
  600. * @returns {boolean} Whether or not `value` is a Promise
  601. */
  602. exports.isPromise = function isPromise(value) {
  603. return typeof value === 'object' && typeof value.then === 'function';
  604. };
  605. /**
  606. * It's a noop.
  607. * @api
  608. */
  609. exports.noop = function() {};