Bug 1093931 - Update Loop mocha unit test framework to v2.0.1, which supports Promises, r=Standard8
authorDan Mosedale <dmose@meer.net>
Fri, 07 Nov 2014 13:59:09 -0800
changeset 226083 1b52d3719b7f25ca1448cd44ccc2f7b3306e7c36
parent 226082 7f0e90fc4932de7e942d93f492f9818590a15719
child 226084 3335d6e68892ceddc6ead115bec9e0af8ed77cf2
push id32
push userbmcbride@mozilla.com
push dateSun, 09 Nov 2014 12:47:32 +0000
reviewersStandard8
bugs1093931
milestone36.0a1
Bug 1093931 - Update Loop mocha unit test framework to v2.0.1, which supports Promises, r=Standard8
browser/components/loop/test/desktop-local/index.html
browser/components/loop/test/shared/index.html
browser/components/loop/test/shared/vendor/mocha-1.17.1.css
browser/components/loop/test/shared/vendor/mocha-1.17.1.js
browser/components/loop/test/shared/vendor/mocha-2.0.1.css
browser/components/loop/test/shared/vendor/mocha-2.0.1.js
browser/components/loop/test/standalone/index.html
browser/components/loop/test/standalone/multiplexGum_test.js
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -1,33 +1,33 @@
 <!DOCTYPE html>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
 <html>
 <head>
   <meta charset="utf-8">
   <title>Loop desktop-local mocha tests</title>
-  <link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
+  <link rel="stylesheet" media="all" href="../shared/vendor/mocha-2.0.1.css">
 </head>
 <body>
   <div id="mocha">
     <p><a href="../">Index</a></p>
   </div>
   <div id="messages"></div>
   <div id="fixtures"></div>
   <!-- libs -->
   <script src="../../content/libs/l10n.js"></script>
   <script src="../../content/shared/libs/react-0.11.2.js"></script>
   <script src="../../content/shared/libs/jquery-2.1.0.js"></script>
   <script src="../../content/shared/libs/lodash-2.4.1.js"></script>
   <script src="../../content/shared/libs/backbone-1.1.2.js"></script>
 
   <!-- test dependencies -->
-  <script src="../shared/vendor/mocha-1.17.1.js"></script>
+  <script src="../shared/vendor/mocha-2.0.1.js"></script>
   <script src="../shared/vendor/chai-1.9.0.js"></script>
   <script src="../shared/vendor/sinon-1.9.0.js"></script>
   <script>
     /*global chai,mocha */
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
 
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -1,34 +1,34 @@
 <!DOCTYPE html>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
 <html>
 <head>
   <meta charset="utf-8">
   <title>Loop shared mocha tests</title>
-  <link rel="stylesheet" media="all" href="vendor/mocha-1.17.1.css">
+  <link rel="stylesheet" media="all" href="vendor/mocha-2.0.1.css">
 </head>
 <body>
   <div id="mocha">
     <p><a href="../">Index</a></p>
   </div>
   <div id="messages"></div>
   <div id="fixtures"></div>
 
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.11.2.js"></script>
   <script src="../../content/shared/libs/jquery-2.1.0.js"></script>
   <script src="../../content/shared/libs/lodash-2.4.1.js"></script>
   <script src="../../content/shared/libs/backbone-1.1.2.js"></script>
   <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
 
   <!-- test dependencies -->
-  <script src="vendor/mocha-1.17.1.js"></script>
+  <script src="vendor/mocha-2.0.1.js"></script>
   <script src="vendor/chai-1.9.0.js"></script>
   <script src="vendor/sinon-1.9.0.js"></script>
   <script>
     /*global chai, mocha */
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
 
rename from browser/components/loop/test/shared/vendor/mocha-1.17.1.css
rename to browser/components/loop/test/shared/vendor/mocha-2.0.1.css
rename from browser/components/loop/test/shared/vendor/mocha-1.17.1.js
rename to browser/components/loop/test/shared/vendor/mocha-2.0.1.js
--- a/browser/components/loop/test/shared/vendor/mocha-1.17.1.js
+++ b/browser/components/loop/test/shared/vendor/mocha-2.0.1.js
@@ -43,17 +43,16 @@ require.relative = function (parent) {
       }
 
       return require(path.join('/'));
     };
   };
 
 
 require.register("browser/debug.js", function(module, exports, require){
-
 module.exports = function(type){
   return function(){
   }
 };
 
 }); // module: browser/debug.js
 
 require.register("browser/diff.js", function(module, exports, require){
@@ -409,18 +408,32 @@ var JsDiff = (function() {
 })();
 
 if (typeof module !== 'undefined') {
     module.exports = JsDiff;
 }
 
 }); // module: browser/diff.js
 
+require.register("browser/escape-string-regexp.js", function(module, exports, require){
+'use strict';
+
+var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
+
+module.exports = function (str) {
+  if (typeof str !== 'string') {
+    throw new TypeError('Expected a string');
+  }
+
+  return str.replace(matchOperatorsRe,  '\\$&');
+};
+
+}); // module: browser/escape-string-regexp.js
+
 require.register("browser/events.js", function(module, exports, require){
-
 /**
  * Module exports.
  */
 
 exports.EventEmitter = EventEmitter;
 
 /**
  * Check if `obj` is an array.
@@ -588,22 +601,27 @@ EventEmitter.prototype.emit = function (
       listeners[i].apply(this, args);
     }
   } else {
     return false;
   }
 
   return true;
 };
+
 }); // module: browser/events.js
 
 require.register("browser/fs.js", function(module, exports, require){
 
 }); // module: browser/fs.js
 
+require.register("browser/glob.js", function(module, exports, require){
+
+}); // module: browser/glob.js
+
 require.register("browser/path.js", function(module, exports, require){
 
 }); // module: browser/path.js
 
 require.register("browser/progress.js", function(module, exports, require){
 /**
  * Expose `Progress`.
  */
@@ -695,67 +713,65 @@ Progress.prototype.draw = function(ctx){
   try {
     var percent = Math.min(this.percent, 100)
       , size = this._size
       , half = size / 2
       , x = half
       , y = half
       , rad = half - 1
       , fontSize = this._fontSize;
-  
+
     ctx.font = fontSize + 'px ' + this._font;
-  
+
     var angle = Math.PI * 2 * (percent / 100);
     ctx.clearRect(0, 0, size, size);
-  
+
     // outer circle
     ctx.strokeStyle = '#9f9f9f';
     ctx.beginPath();
     ctx.arc(x, y, rad, 0, angle, false);
     ctx.stroke();
-  
+
     // inner circle
     ctx.strokeStyle = '#eee';
     ctx.beginPath();
     ctx.arc(x, y, rad - 1, 0, angle, true);
     ctx.stroke();
-  
+
     // text
     var text = this._text || (percent | 0) + '%'
       , w = ctx.measureText(text).width;
-  
+
     ctx.fillText(
         text
       , x - w / 2 + 1
       , y + fontSize / 2 - 1);
   } catch (ex) {} //don't fail if we can't render progress
   return this;
 };
 
 }); // module: browser/progress.js
 
 require.register("browser/tty.js", function(module, exports, require){
-
 exports.isatty = function(){
   return true;
 };
 
 exports.getWindowSize = function(){
   if ('innerHeight' in global) {
     return [global.innerHeight, global.innerWidth];
   } else {
     // In a Web Worker, the DOM Window is not available.
     return [640, 480];
   }
 };
 
 }); // module: browser/tty.js
 
 require.register("context.js", function(module, exports, require){
-
 /**
  * Expose `Context`.
  */
 
 module.exports = Context;
 
 /**
  * Initialize a new `Context`.
@@ -783,21 +799,36 @@ Context.prototype.runnable = function(ru
  * Set test timeout `ms`.
  *
  * @param {Number} ms
  * @return {Context} self
  * @api private
  */
 
 Context.prototype.timeout = function(ms){
+  if (arguments.length === 0) return this.runnable().timeout();
   this.runnable().timeout(ms);
   return this;
 };
 
 /**
+ * Set test timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Context} self
+ * @api private
+ */
+
+Context.prototype.enableTimeouts = function (enabled) {
+  this.runnable().enableTimeouts(enabled);
+  return this;
+};
+
+
+/**
  * Set test slowness threshold `ms`.
  *
  * @param {Number} ms
  * @return {Context} self
  * @api private
  */
 
 Context.prototype.slow = function(ms){
@@ -818,17 +849,16 @@ Context.prototype.inspect = function(){
     if ('test' == key) return;
     return val;
   }, 2);
 };
 
 }); // module: context.js
 
 require.register("hook.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Runnable = require('./runnable');
 
 /**
  * Expose `Hook`.
@@ -875,24 +905,24 @@ Hook.prototype.error = function(err){
   }
 
   this._error = err;
 };
 
 }); // module: hook.js
 
 require.register("interfaces/bdd.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Suite = require('../suite')
   , Test = require('../test')
-  , utils = require('../utils');
+  , utils = require('../utils')
+  , escapeRe = require('browser/escape-string-regexp');
 
 /**
  * BDD-style interface:
  *
  *      describe('Array', function(){
  *        describe('#indexOf()', function(){
  *          it('should return -1 when not present', function(){
  *
@@ -910,52 +940,53 @@ module.exports = function(suite){
   var suites = [suite];
 
   suite.on('pre-require', function(context, file, mocha){
 
     /**
      * Execute before running tests.
      */
 
-    context.before = function(fn){
-      suites[0].beforeAll(fn);
+    context.before = function(name, fn){
+      suites[0].beforeAll(name, fn);
     };
 
     /**
      * Execute after running tests.
      */
 
-    context.after = function(fn){
-      suites[0].afterAll(fn);
+    context.after = function(name, fn){
+      suites[0].afterAll(name, fn);
     };
 
     /**
      * Execute before each test case.
      */
 
-    context.beforeEach = function(fn){
-      suites[0].beforeEach(fn);
+    context.beforeEach = function(name, fn){
+      suites[0].beforeEach(name, fn);
     };
 
     /**
      * Execute after each test case.
      */
 
-    context.afterEach = function(fn){
-      suites[0].afterEach(fn);
+    context.afterEach = function(name, fn){
+      suites[0].afterEach(name, fn);
     };
 
     /**
      * Describe a "suite" with the given `title`
      * and callback `fn` containing nested suites
      * and/or tests.
      */
 
     context.describe = context.context = function(title, fn){
       var suite = Suite.create(suites[0], title);
+      suite.file = file;
       suites.unshift(suite);
       fn.call(suite);
       suites.shift();
       return suite;
     };
 
     /**
      * Pending describe.
@@ -984,29 +1015,30 @@ module.exports = function(suite){
     /**
      * Describe a specification or test-case
      * with the given `title` and callback `fn`
      * acting as a thunk.
      */
 
     context.it = context.specify = function(title, fn){
       var suite = suites[0];
-      if (suite.pending) var fn = null;
+      if (suite.pending) fn = null;
       var test = new Test(title, fn);
+      test.file = file;
       suite.addTest(test);
       return test;
     };
 
     /**
      * Exclusive test-case.
      */
 
     context.it.only = function(title, fn){
       var test = context.it(title, fn);
-      var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$';
+      var reString = '^' + escapeRe(test.fullTitle()) + '$';
       mocha.grep(new RegExp(reString));
       return test;
     };
 
     /**
      * Pending test case.
      */
 
@@ -1016,17 +1048,16 @@ module.exports = function(suite){
       context.it(title);
     };
   });
 };
 
 }); // module: interfaces/bdd.js
 
 require.register("interfaces/exports.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Suite = require('../suite')
   , Test = require('../test');
 
 /**
@@ -1046,17 +1077,17 @@ var Suite = require('../suite')
  *
  */
 
 module.exports = function(suite){
   var suites = [suite];
 
   suite.on('require', visit);
 
-  function visit(obj) {
+  function visit(obj, file) {
     var suite;
     for (var key in obj) {
       if ('function' == typeof obj[key]) {
         var fn = obj[key];
         switch (key) {
           case 'before':
             suites[0].beforeAll(fn);
             break;
@@ -1065,47 +1096,48 @@ module.exports = function(suite){
             break;
           case 'beforeEach':
             suites[0].beforeEach(fn);
             break;
           case 'afterEach':
             suites[0].afterEach(fn);
             break;
           default:
-            suites[0].addTest(new Test(key, fn));
+            var test = new Test(key, fn);
+            test.file = file;
+            suites[0].addTest(test);
         }
       } else {
-        var suite = Suite.create(suites[0], key);
+        suite = Suite.create(suites[0], key);
         suites.unshift(suite);
         visit(obj[key]);
         suites.shift();
       }
     }
   }
 };
 
 }); // module: interfaces/exports.js
 
 require.register("interfaces/index.js", function(module, exports, require){
-
 exports.bdd = require('./bdd');
 exports.tdd = require('./tdd');
 exports.qunit = require('./qunit');
 exports.exports = require('./exports');
 
 }); // module: interfaces/index.js
 
 require.register("interfaces/qunit.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Suite = require('../suite')
   , Test = require('../test')
+  , escapeRe = require('browser/escape-string-regexp')
   , utils = require('../utils');
 
 /**
  * QUnit-style interface:
  *
  *     suite('Array');
  *
  *     test('#length', function(){
@@ -1132,51 +1164,52 @@ module.exports = function(suite){
   var suites = [suite];
 
   suite.on('pre-require', function(context, file, mocha){
 
     /**
      * Execute before running tests.
      */
 
-    context.before = function(fn){
-      suites[0].beforeAll(fn);
+    context.before = function(name, fn){
+      suites[0].beforeAll(name, fn);
     };
 
     /**
      * Execute after running tests.
      */
 
-    context.after = function(fn){
-      suites[0].afterAll(fn);
+    context.after = function(name, fn){
+      suites[0].afterAll(name, fn);
     };
 
     /**
      * Execute before each test case.
      */
 
-    context.beforeEach = function(fn){
-      suites[0].beforeEach(fn);
+    context.beforeEach = function(name, fn){
+      suites[0].beforeEach(name, fn);
     };
 
     /**
      * Execute after each test case.
      */
 
-    context.afterEach = function(fn){
-      suites[0].afterEach(fn);
+    context.afterEach = function(name, fn){
+      suites[0].afterEach(name, fn);
     };
 
     /**
      * Describe a "suite" with the given `title`.
      */
 
     context.suite = function(title){
       if (suites.length > 1) suites.shift();
       var suite = Suite.create(suites[0], title);
+      suite.file = file;
       suites.unshift(suite);
       return suite;
     };
 
     /**
      * Exclusive test-case.
      */
 
@@ -1188,51 +1221,52 @@ module.exports = function(suite){
     /**
      * Describe a specification or test-case
      * with the given `title` and callback `fn`
      * acting as a thunk.
      */
 
     context.test = function(title, fn){
       var test = new Test(title, fn);
+      test.file = file;
       suites[0].addTest(test);
       return test;
     };
 
     /**
      * Exclusive test-case.
      */
 
     context.test.only = function(title, fn){
       var test = context.test(title, fn);
-      var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$';
+      var reString = '^' + escapeRe(test.fullTitle()) + '$';
       mocha.grep(new RegExp(reString));
     };
 
     /**
      * Pending test case.
      */
 
     context.test.skip = function(title){
       context.test(title);
     };
   });
 };
 
 }); // module: interfaces/qunit.js
 
 require.register("interfaces/tdd.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Suite = require('../suite')
   , Test = require('../test')
-  , utils = require('../utils');;
+  , escapeRe = require('browser/escape-string-regexp')
+  , utils = require('../utils');
 
 /**
  * TDD-style interface:
  *
  *      suite('Array', function(){
  *        suite('#indexOf()', function(){
  *          suiteSetup(function(){
  *
@@ -1258,52 +1292,53 @@ module.exports = function(suite){
   var suites = [suite];
 
   suite.on('pre-require', function(context, file, mocha){
 
     /**
      * Execute before each test case.
      */
 
-    context.setup = function(fn){
-      suites[0].beforeEach(fn);
+    context.setup = function(name, fn){
+      suites[0].beforeEach(name, fn);
     };
 
     /**
      * Execute after each test case.
      */
 
-    context.teardown = function(fn){
-      suites[0].afterEach(fn);
+    context.teardown = function(name, fn){
+      suites[0].afterEach(name, fn);
     };
 
     /**
      * Execute before the suite.
      */
 
-    context.suiteSetup = function(fn){
-      suites[0].beforeAll(fn);
+    context.suiteSetup = function(name, fn){
+      suites[0].beforeAll(name, fn);
     };
 
     /**
      * Execute after the suite.
      */
 
-    context.suiteTeardown = function(fn){
-      suites[0].afterAll(fn);
+    context.suiteTeardown = function(name, fn){
+      suites[0].afterAll(name, fn);
     };
 
     /**
      * Describe a "suite" with the given `title`
      * and callback `fn` containing nested suites
      * and/or tests.
      */
 
     context.suite = function(title, fn){
       var suite = Suite.create(suites[0], title);
+      suite.file = file;
       suites.unshift(suite);
       fn.call(suite);
       suites.shift();
       return suite;
     };
 
     /**
      * Pending suite.
@@ -1328,29 +1363,30 @@ module.exports = function(suite){
     /**
      * Describe a specification or test-case
      * with the given `title` and callback `fn`
      * acting as a thunk.
      */
 
     context.test = function(title, fn){
       var suite = suites[0];
-      if (suite.pending) var fn = null;
+      if (suite.pending) fn = null;
       var test = new Test(title, fn);
+      test.file = file;
       suite.addTest(test);
       return test;
     };
 
     /**
      * Exclusive test-case.
      */
 
     context.test.only = function(title, fn){
       var test = context.test(title, fn);
-      var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$';
+      var reString = '^' + escapeRe(test.fullTitle()) + '$';
       mocha.grep(new RegExp(reString));
     };
 
     /**
      * Pending test case.
      */
 
     context.test.skip = function(title){
@@ -1368,25 +1404,36 @@ require.register("mocha.js", function(mo
  * MIT Licensed
  */
 
 /**
  * Module dependencies.
  */
 
 var path = require('browser/path')
+  , escapeRe = require('browser/escape-string-regexp')
   , utils = require('./utils');
 
 /**
  * Expose `Mocha`.
  */
 
 exports = module.exports = Mocha;
 
 /**
+ * To require local UIs and reporters when running in node.
+ */
+
+if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
+  var join = path.join
+    , cwd = process.cwd();
+  module.paths.push(cwd, join(cwd, 'node_modules'));
+}
+
+/**
  * Expose internals.
  */
 
 exports.utils = utils;
 exports.interfaces = require('./interfaces');
 exports.reporters = require('./reporters');
 exports.Runnable = require('./runnable');
 exports.Context = require('./context');
@@ -1408,17 +1455,17 @@ function image(name) {
 }
 
 /**
  * Setup mocha with `options`.
  *
  * Options:
  *
  *   - `ui` name "bdd", "tdd", "exports" etc
- *   - `reporter` reporter instance, defaults to `mocha.reporters.Dot`
+ *   - `reporter` reporter instance, defaults to `mocha.reporters.spec`
  *   - `globals` array of accepted globals
  *   - `timeout` timeout in milliseconds
  *   - `bail` bail on the first test failure
  *   - `slow` milliseconds to wait before considering a test slow
  *   - `ignoreLeaks` ignore global leaks
  *   - `grep` string or regexp to filter tests with
  *
  * @param {Object} options
@@ -1431,16 +1478,17 @@ function Mocha(options) {
   this.options = options;
   this.grep(options.grep);
   this.suite = new exports.Suite('', new exports.Context);
   this.ui(options.ui);
   this.bail(options.bail);
   this.reporter(options.reporter);
   if (null != options.timeout) this.timeout(options.timeout);
   this.useColors(options.useColors)
+  if (options.enableTimeouts !== null) this.enableTimeouts(options.enableTimeouts);
   if (options.slow) this.slow(options.slow);
 
   this.suite.on('pre-require', function (context) {
     exports.afterEach = context.afterEach || context.teardown;
     exports.after = context.after || context.suiteTeardown;
     exports.beforeEach = context.beforeEach || context.setup;
     exports.before = context.before || context.suiteSetup;
     exports.describe = context.describe || context.suite;
@@ -1475,27 +1523,27 @@ Mocha.prototype.bail = function(bail){
  */
 
 Mocha.prototype.addFile = function(file){
   this.files.push(file);
   return this;
 };
 
 /**
- * Set reporter to `reporter`, defaults to "dot".
+ * Set reporter to `reporter`, defaults to "spec".
  *
  * @param {String|Function} reporter name or constructor
  * @api public
  */
 
 Mocha.prototype.reporter = function(reporter){
   if ('function' == typeof reporter) {
     this._reporter = reporter;
   } else {
-    reporter = reporter || 'dot';
+    reporter = reporter || 'spec';
     var _reporter;
     try { _reporter = require('./reporters/' + reporter); } catch (err) {};
     if (!_reporter) try { _reporter = require(reporter); } catch (err) {};
     if (!_reporter && reporter === 'teamcity')
       console.warn('The Teamcity reporter was moved to a package named ' +
         'mocha-teamcity-reporter ' +
         '(https://npmjs.org/package/mocha-teamcity-reporter).');
     if (!_reporter) throw new Error('invalid reporter "' + reporter + '"');
@@ -1568,17 +1616,17 @@ Mocha.prototype._growl = function(runner
  *
  * @param {RegExp|String} re
  * @return {Mocha}
  * @api public
  */
 
 Mocha.prototype.grep = function(re){
   this.options.grep = 'string' == typeof re
-    ? new RegExp(utils.escapeRegexp(re))
+    ? new RegExp(escapeRe(re))
     : re;
   return this;
 };
 
 /**
  * Invert `.grep()` matches.
  *
  * @return {Mocha}
@@ -1692,41 +1740,67 @@ Mocha.prototype.timeout = function(timeo
  */
 
 Mocha.prototype.slow = function(slow){
   this.suite.slow(slow);
   return this;
 };
 
 /**
+ * Enable timeouts.
+ *
+ * @param {Boolean} enabled
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.enableTimeouts = function(enabled) {
+  this.suite.enableTimeouts(arguments.length && enabled !== undefined
+    ? enabled
+    : true);
+  return this
+};
+
+/**
  * Makes all tests async (accepting a callback)
  *
  * @return {Mocha}
  * @api public
  */
 
 Mocha.prototype.asyncOnly = function(){
   this.options.asyncOnly = true;
   return this;
 };
 
 /**
+ * Disable syntax highlighting (in browser).
+ * @returns {Mocha}
+ * @api public
+ */
+Mocha.prototype.noHighlighting = function() {
+  this.options.noHighlighting = true;
+  return this;
+};
+
+/**
  * Run tests and invoke `fn()` when complete.
  *
  * @param {Function} fn
  * @return {Runner}
  * @api public
  */
 
 Mocha.prototype.run = function(fn){
   if (this.files.length) this.loadFiles();
   var suite = this.suite;
   var options = this.options;
+  options.files = this.files;
   var runner = new exports.Runner(suite);
-  var reporter = new this._reporter(runner);
+  var reporter = new this._reporter(runner, options);
   runner.ignoreLeaks = false !== options.ignoreLeaks;
   runner.asyncOnly = options.asyncOnly;
   if (options.grep) runner.grep(options.grep, options.invert);
   if (options.globals) runner.globals(options.globals);
   if (options.growl) this._growl(runner, reporter);
   exports.reporters.Base.useColors = options.useColors;
   exports.reporters.Base.inlineDiffs = options.useInlineDiffs;
   return runner.run(fn);
@@ -1756,17 +1830,17 @@ var y = d * 365.25;
  * @param {Object} options
  * @return {String|Number}
  * @api public
  */
 
 module.exports = function(val, options){
   options = options || {};
   if ('string' == typeof val) return parse(val);
-  return options.long ? longFormat(val) : shortFormat(val);
+  return options['long'] ? longFormat(val) : shortFormat(val);
 };
 
 /**
  * Parse the given `str` and return milliseconds.
  *
  * @param {String} str
  * @return {Number}
  * @api private
@@ -1843,17 +1917,16 @@ function plural(ms, n, name) {
   if (ms < n) return;
   if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
   return Math.ceil(ms / n) + ' ' + name + 's';
 }
 
 }); // module: ms.js
 
 require.register("reporters/base.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var tty = require('browser/tty')
   , diff = require('browser/diff')
   , ms = require('../ms')
   , utils = require('../utils');
@@ -2025,22 +2098,22 @@ exports.list = function(failures){
     // uncaught
     if (err.uncaught) {
       msg = 'Uncaught ' + msg;
     }
 
     // explicitly show diff
     if (err.showDiff && sameType(actual, expected)) {
       escape = false;
-      err.actual = actual = stringify(canonicalize(actual));
-      err.expected = expected = stringify(canonicalize(expected));
+      err.actual = actual = utils.stringify(actual);
+      err.expected = expected = utils.stringify(expected);
     }
 
     // actual / expected diff
-    if ('string' == typeof actual && 'string' == typeof expected) {
+    if (err.showDiff && 'string' == typeof actual && 'string' == typeof expected) {
       fmt = color('error title', '  %s) %s:\n%s') + color('error stack', '\n%s\n');
       var match = message.match(/^([^:]+): expected/);
       msg = '\n      ' + color('error message', match ? match[1] : msg);
 
       if (exports.inlineDiffs) {
         msg += inlineDiff(err, escape);
       } else {
         msg += unifiedDiff(err, escape);
@@ -2289,82 +2362,33 @@ function escapeInvisibles(line) {
 
 function colorLines(name, str) {
   return str.split('\n').map(function(str){
     return color(name, str);
   }).join('\n');
 }
 
 /**
- * Stringify `obj`.
- *
- * @param {Object} obj
- * @return {String}
- * @api private
- */
-
-function stringify(obj) {
-  if (obj instanceof RegExp) return obj.toString();
-  return JSON.stringify(obj, null, 2);
-}
-
-/**
- * Return a new object that has the keys in sorted order.
- * @param {Object} obj
- * @return {Object}
- * @api private
- */
-
- function canonicalize(obj, stack) {
-   stack = stack || [];
-
-   if (utils.indexOf(stack, obj) !== -1) return obj;
-
-   var canonicalizedObj;
-
-   if ('[object Array]' == {}.toString.call(obj)) {
-     stack.push(obj);
-     canonicalizedObj = utils.map(obj, function(item) {
-       return canonicalize(item, stack);
-     });
-     stack.pop();
-   } else if (typeof obj === 'object' && obj !== null) {
-     stack.push(obj);
-     canonicalizedObj = {};
-     utils.forEach(utils.keys(obj).sort(), function(key) {
-       canonicalizedObj[key] = canonicalize(obj[key], stack);
-     });
-     stack.pop();
-   } else {
-     canonicalizedObj = obj;
-   }
-
-   return canonicalizedObj;
- }
-
-/**
  * Check that a / b have the same type.
  *
  * @param {Object} a
  * @param {Object} b
  * @return {Boolean}
  * @api private
  */
 
 function sameType(a, b) {
   a = Object.prototype.toString.call(a);
   b = Object.prototype.toString.call(b);
   return a == b;
 }
 
-
 }); // module: reporters/base.js
 
 require.register("reporters/doc.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , utils = require('../utils');
 
 /**
@@ -2409,22 +2433,28 @@ function Doc(runner) {
     --indents;
   });
 
   runner.on('pass', function(test){
     console.log('%s  <dt>%s</dt>', indent(), utils.escape(test.title));
     var code = utils.escape(utils.clean(test.fn.toString()));
     console.log('%s  <dd><pre><code>%s</code></pre></dd>', indent(), code);
   });
+
+  runner.on('fail', function(test, err){
+    console.log('%s  <dt class="error">%s</dt>', indent(), utils.escape(test.title));
+    var code = utils.escape(utils.clean(test.fn.toString()));
+    console.log('%s  <dd class="error"><pre><code>%s</code></pre></dd>', indent(), code);
+    console.log('%s  <dd class="error">%s</dd>', indent(), utils.escape(err));
+  });
 }
 
 }); // module: reporters/doc.js
 
 require.register("reporters/dot.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , color = Base.color;
 
 /**
@@ -2441,23 +2471,24 @@ exports = module.exports = Dot;
  */
 
 function Dot(runner) {
   Base.call(this, runner);
 
   var self = this
     , stats = this.stats
     , width = Base.window.width * .75 | 0
-    , n = 0;
+    , n = -1;
 
   runner.on('start', function(){
     process.stdout.write('\n  ');
   });
 
   runner.on('pending', function(test){
+    if (++n % width == 0) process.stdout.write('\n  ');
     process.stdout.write(color('pending', Base.symbols.dot));
   });
 
   runner.on('pass', function(test){
     if (++n % width == 0) process.stdout.write('\n  ');
     if ('slow' == test.speed) {
       process.stdout.write(color('bright yellow', Base.symbols.dot));
     } else {
@@ -2480,20 +2511,20 @@ function Dot(runner) {
  * Inherit from `Base.prototype`.
  */
 
 function F(){};
 F.prototype = Base.prototype;
 Dot.prototype = new F;
 Dot.prototype.constructor = Dot;
 
+
 }); // module: reporters/dot.js
 
 require.register("reporters/html-cov.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var JSONCov = require('./json-cov')
   , fs = require('browser/fs');
 
 /**
@@ -2534,20 +2565,20 @@ function HTMLCov(runner) {
  */
 
 function coverageClass(n) {
   if (n >= 75) return 'high';
   if (n >= 50) return 'medium';
   if (n >= 25) return 'low';
   return 'terrible';
 }
+
 }); // module: reporters/html-cov.js
 
 require.register("reporters/html.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , utils = require('../utils')
   , Progress = require('../browser/progress')
   , escape = utils.escape;
@@ -2581,17 +2612,17 @@ var statsTemplate = '<ul id="mocha-stats
 
 /**
  * Initialize a new `HTML` reporter.
  *
  * @param {Runner} runner
  * @api public
  */
 
-function HTML(runner, root) {
+function HTML(runner) {
   Base.call(this, runner);
 
   var self = this
     , stats = this.stats
     , total = runner.total
     , stat = fragment(statsTemplate)
     , items = stat.getElementsByTagName('li')
     , passes = items[1].getElementsByTagName('em')[0]
@@ -2599,18 +2630,17 @@ function HTML(runner, root) {
     , failures = items[2].getElementsByTagName('em')[0]
     , failuresLink = items[2].getElementsByTagName('a')[0]
     , duration = items[3].getElementsByTagName('em')[0]
     , canvas = stat.getElementsByTagName('canvas')[0]
     , report = fragment('<ul id="mocha-report"></ul>')
     , stack = [report]
     , progress
     , ctx
-
-  root = root || document.getElementById('mocha');
+    , root = document.getElementById('mocha');
 
   if (canvas.getContext) {
     var ratio = window.devicePixelRatio || 1;
     canvas.style.width = canvas.width;
     canvas.style.height = canvas.height;
     canvas.width *= ratio;
     canvas.height *= ratio;
     ctx = canvas.getContext('2d');
@@ -2718,33 +2748,42 @@ function HTML(runner, root) {
     }
 
     // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack.
     if (stack[0]) stack[0].appendChild(el);
   });
 }
 
 /**
+ * Makes a URL, preserving querystring ("search") parameters.
+ * @param {string} s
+ * @returns {string} your new URL
+ */
+var makeUrl = function makeUrl(s) {
+  var search = window.location.search;
+  return (search ? search + '&' : '?' ) + 'grep=' + encodeURIComponent(s);
+};
+
+/**
  * Provide suite URL
  *
  * @param {Object} [suite]
  */
-
 HTML.prototype.suiteURL = function(suite){
-  return '?grep=' + encodeURIComponent(suite.fullTitle());
+  return makeUrl(suite.fullTitle());
 };
 
 /**
  * Provide test URL
  *
  * @param {Object} [test]
  */
 
 HTML.prototype.testURL = function(test){
-  return '?grep=' + encodeURIComponent(test.fullTitle());
+  return makeUrl(test.fullTitle());
 };
 
 /**
  * Display error `msg`.
  */
 
 function error(msg) {
   document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
@@ -2815,17 +2854,16 @@ function on(el, event, fn) {
   } else {
     el.attachEvent('on' + event, fn);
   }
 }
 
 }); // module: reporters/html.js
 
 require.register("reporters/index.js", function(module, exports, require){
-
 exports.Base = require('./base');
 exports.Dot = require('./dot');
 exports.Doc = require('./doc');
 exports.TAP = require('./tap');
 exports.JSON = require('./json');
 exports.HTML = require('./html');
 exports.List = require('./list');
 exports.Min = require('./min');
@@ -2837,17 +2875,16 @@ exports.Progress = require('./progress')
 exports.Landing = require('./landing');
 exports.JSONCov = require('./json-cov');
 exports.HTMLCov = require('./html-cov');
 exports.JSONStream = require('./json-stream');
 
 }); // module: reporters/index.js
 
 require.register("reporters/json-cov.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base');
 
 /**
  * Expose `JSONCov`.
@@ -2928,17 +2965,17 @@ function map(cov) {
     return a.filename.localeCompare(b.filename);
   });
 
   if (ret.sloc > 0) {
     ret.coverage = (ret.hits / ret.sloc) * 100;
   }
 
   return ret;
-};
+}
 
 /**
  * Map jscoverage data for a single source file
  * to a JSON structure suitable for reporting.
  *
  * @param {String} filename name of the source file
  * @param {Object} data jscoverage coverage data
  * @return {Object}
@@ -2994,17 +3031,16 @@ function clean(test) {
     , fullTitle: test.fullTitle()
     , duration: test.duration
   }
 }
 
 }); // module: reporters/json-cov.js
 
 require.register("reporters/json-stream.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , color = Base.color;
 
 /**
@@ -3031,17 +3067,19 @@ function List(runner) {
     console.log(JSON.stringify(['start', { total: total }]));
   });
 
   runner.on('pass', function(test){
     console.log(JSON.stringify(['pass', clean(test)]));
   });
 
   runner.on('fail', function(test, err){
-    console.log(JSON.stringify(['fail', clean(test)]));
+    test = clean(test);
+    test.err = err.message;
+    console.log(JSON.stringify(['fail', test]));
   });
 
   runner.on('end', function(){
     process.stdout.write(JSON.stringify(['end', self.stats]));
   });
 }
 
 /**
@@ -3055,20 +3093,20 @@ function List(runner) {
 
 function clean(test) {
   return {
       title: test.title
     , fullTitle: test.fullTitle()
     , duration: test.duration
   }
 }
+
 }); // module: reporters/json-stream.js
 
 require.register("reporters/json.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , cursor = Base.cursor
   , color = Base.color;
 
@@ -3085,63 +3123,86 @@ exports = module.exports = JSONReporter;
  * @api public
  */
 
 function JSONReporter(runner) {
   var self = this;
   Base.call(this, runner);
 
   var tests = []
+    , pending = []
     , failures = []
     , passes = [];
 
   runner.on('test end', function(test){
     tests.push(test);
   });
 
   runner.on('pass', function(test){
     passes.push(test);
   });
 
   runner.on('fail', function(test){
     failures.push(test);
   });
 
+  runner.on('pending', function(test){
+    pending.push(test);
+  });
+
   runner.on('end', function(){
     var obj = {
-        stats: self.stats
-      , tests: tests.map(clean)
-      , failures: failures.map(clean)
-      , passes: passes.map(clean)
+      stats: self.stats,
+      tests: tests.map(clean),
+      pending: pending.map(clean),
+      failures: failures.map(clean),
+      passes: passes.map(clean)
     };
 
+    runner.testResults = obj;
+
     process.stdout.write(JSON.stringify(obj, null, 2));
   });
 }
 
 /**
  * Return a plain-object representation of `test`
  * free of cyclic properties etc.
  *
  * @param {Object} test
  * @return {Object}
  * @api private
  */
 
 function clean(test) {
   return {
-      title: test.title
-    , fullTitle: test.fullTitle()
-    , duration: test.duration
+    title: test.title,
+    fullTitle: test.fullTitle(),
+    duration: test.duration,
+    err: errorJSON(test.err || {})
   }
 }
+
+/**
+ * Transform `error` into a JSON object.
+ * @param {Error} err
+ * @return {Object}
+ */
+
+function errorJSON(err) {
+  var res = {};
+  Object.getOwnPropertyNames(err).forEach(function(key) {
+    res[key] = err[key];
+  }, err);
+  return res;
+}
+
 }); // module: reporters/json.js
 
 require.register("reporters/landing.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , cursor = Base.cursor
   , color = Base.color;
 
@@ -3189,34 +3250,34 @@ function Landing(runner) {
     , n = 0;
 
   function runway() {
     var buf = Array(width).join('-');
     return '  ' + color('runway', buf);
   }
 
   runner.on('start', function(){
-    stream.write('\n  ');
+    stream.write('\n\n\n  ');
     cursor.hide();
   });
 
   runner.on('test end', function(test){
     // check if the plane crashed
     var col = -1 == crashed
       ? width * ++n / total | 0
       : crashed;
 
     // show the crash
     if ('failed' == test.state) {
       plane = color('plane crash', '✈');
       crashed = col;
     }
 
     // render landing strip
-    stream.write('\u001b[4F\n\n');
+    stream.write('\u001b['+(width+1)+'D\u001b[2A');
     stream.write(runway());
     stream.write('\n  ');
     stream.write(color('runway', Array(col).join('⋅')));
     stream.write(plane)
     stream.write(color('runway', Array(width - col).join('⋅') + '\n'));
     stream.write(runway());
     stream.write('\u001b[0m');
   });
@@ -3232,20 +3293,20 @@ function Landing(runner) {
  * Inherit from `Base.prototype`.
  */
 
 function F(){};
 F.prototype = Base.prototype;
 Landing.prototype = new F;
 Landing.prototype.constructor = Landing;
 
+
 }); // module: reporters/landing.js
 
 require.register("reporters/list.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , cursor = Base.cursor
   , color = Base.color;
 
@@ -3398,20 +3459,20 @@ function Markdown(runner) {
   });
 
   runner.on('end', function(){
     process.stdout.write('# TOC\n');
     process.stdout.write(generateTOC(runner.suite));
     process.stdout.write(buf);
   });
 }
+
 }); // module: reporters/markdown.js
 
 require.register("reporters/min.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base');
 
 /**
  * Expose `Min`.
@@ -3634,17 +3695,17 @@ NyanCat.prototype.face = function() {
     return '( x .x)';
   } else if (stats.pending) {
     return '( o .o)';
   } else if(stats.passes) {
     return '( ^ .^)';
   } else {
     return '( - .-)';
   }
-}
+};
 
 /**
  * Move cursor up `n`.
  *
  * @param {Number} n
  * @api private
  */
 
@@ -3715,17 +3776,16 @@ function F(){};
 F.prototype = Base.prototype;
 NyanCat.prototype = new F;
 NyanCat.prototype.constructor = NyanCat;
 
 
 }); // module: reporters/nyan.js
 
 require.register("reporters/progress.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , cursor = Base.cursor
   , color = Base.color;
 
@@ -3753,17 +3813,18 @@ function Progress(runner, options) {
   Base.call(this, runner);
 
   var self = this
     , options = options || {}
     , stats = this.stats
     , width = Base.window.width * .50 | 0
     , total = runner.total
     , complete = 0
-    , max = Math.max;
+    , max = Math.max
+    , lastN = -1;
 
   // default chars
   options.open = options.open || '[';
   options.complete = options.complete || '▬';
   options.incomplete = options.incomplete || Base.symbols.dot;
   options.close = options.close || ']';
   options.verbose = false;
 
@@ -3776,16 +3837,22 @@ function Progress(runner, options) {
   // tests complete
   runner.on('test end', function(){
     complete++;
     var incomplete = total - complete
       , percent = complete / total
       , n = width * percent | 0
       , i = width - n;
 
+    if (lastN === n && !options.verbose) {
+      // Don't re-render the line if it hasn't changed
+      return;
+    }
+    lastN = n;
+
     cursor.CR();
     process.stdout.write('\u001b[J');
     process.stdout.write(color('progress', '  ' + options.open));
     process.stdout.write(Array(n).join(options.complete));
     process.stdout.write(Array(i).join(options.incomplete));
     process.stdout.write(color('progress', options.close));
     if (options.verbose) {
       process.stdout.write(color('progress', ' ' + complete + ' of ' + total));
@@ -3809,17 +3876,16 @@ function F(){};
 F.prototype = Base.prototype;
 Progress.prototype = new F;
 Progress.prototype.constructor = Progress;
 
 
 }); // module: reporters/progress.js
 
 require.register("reporters/spec.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , cursor = Base.cursor
   , color = Base.color;
 
@@ -3900,17 +3966,16 @@ function F(){};
 F.prototype = Base.prototype;
 Spec.prototype = new F;
 Spec.prototype.constructor = Spec;
 
 
 }); // module: reporters/spec.js
 
 require.register("reporters/tap.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , cursor = Base.cursor
   , color = Base.color;
 
@@ -3977,17 +4042,16 @@ function TAP(runner) {
 
 function title(test) {
   return test.fullTitle().replace(/#/g, '');
 }
 
 }); // module: reporters/tap.js
 
 require.register("reporters/xunit.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Base = require('./base')
   , utils = require('../utils')
   , escape = utils.escape;
 
@@ -4066,18 +4130,17 @@ function test(test) {
   var attrs = {
       classname: test.parent.fullTitle()
     , name: test.title
     , time: (test.duration / 1000) || 0
   };
 
   if ('failed' == test.state) {
     var err = test.err;
-    attrs.message = escape(err.message);
-    console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack))));
+    console.log(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack))));
   } else if (test.pending) {
     console.log(tag('testcase', attrs, false, tag('skipped', {}, true)));
   } else {
     console.log(tag('testcase', attrs, true) );
   }
 }
 
 /**
@@ -4104,17 +4167,16 @@ function tag(name, attrs, close, content
 
 function cdata(str) {
   return '<![CDATA[' + escape(str) + ']]>';
 }
 
 }); // module: reporters/xunit.js
 
 require.register("runnable.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var EventEmitter = require('browser/events').EventEmitter
   , debug = require('browser/debug')('mocha:runnable')
   , milliseconds = require('./ms');
 
@@ -4150,17 +4212,19 @@ module.exports = Runnable;
 
 function Runnable(title, fn) {
   this.title = title;
   this.fn = fn;
   this.async = fn && fn.length;
   this.sync = ! this.async;
   this._timeout = 2000;
   this._slow = 75;
+  this._enableTimeouts = true;
   this.timedOut = false;
+  this._trace = new Error('done() called multiple times')
 }
 
 /**
  * Inherit from `EventEmitter.prototype`.
  */
 
 function F(){};
 F.prototype = EventEmitter.prototype;
@@ -4173,16 +4237,17 @@ Runnable.prototype.constructor = Runnabl
  *
  * @param {Number|String} ms
  * @return {Runnable|Number} ms or self
  * @api private
  */
 
 Runnable.prototype.timeout = function(ms){
   if (0 == arguments.length) return this._timeout;
+  if (ms === 0) this._enableTimeouts = false;
   if ('string' == typeof ms) ms = milliseconds(ms);
   debug('timeout %d', ms);
   this._timeout = ms;
   if (this.timer) this.resetTimeout();
   return this;
 };
 
 /**
@@ -4197,16 +4262,31 @@ Runnable.prototype.slow = function(ms){
   if (0 === arguments.length) return this._slow;
   if ('string' == typeof ms) ms = milliseconds(ms);
   debug('timeout %d', ms);
   this._slow = ms;
   return this;
 };
 
 /**
+ * Set and & get timeout `enabled`.
+ *
+ * @param {Boolean} enabled
+ * @return {Runnable|Boolean} enabled or self
+ * @api private
+ */
+
+Runnable.prototype.enableTimeouts = function(enabled){
+  if (arguments.length === 0) return this._enableTimeouts;
+  debug('enableTimeouts %s', enabled);
+  this._enableTimeouts = enabled;
+  return this;
+};
+
+/**
  * Return the full title generated by recursively
  * concatenating the parent's full title.
  *
  * @return {String}
  * @api public
  */
 
 Runnable.prototype.fullTitle = function(){
@@ -4244,18 +4324,20 @@ Runnable.prototype.inspect = function(){
  *
  * @api private
  */
 
 Runnable.prototype.resetTimeout = function(){
   var self = this;
   var ms = this.timeout() || 1e9;
 
+  if (!this._enableTimeouts) return;
   this.clearTimeout();
   this.timer = setTimeout(function(){
+    if (!self._enableTimeouts) return;
     self.callback(new Error('timeout of ' + ms + 'ms exceeded'));
     self.timedOut = true;
   }, ms);
 };
 
 /**
  * Whitelist these globals for this test run
  *
@@ -4270,79 +4352,97 @@ Runnable.prototype.globals = function(ar
  * Run the test and invoke `fn(err)`.
  *
  * @param {Function} fn
  * @api private
  */
 
 Runnable.prototype.run = function(fn){
   var self = this
-    , ms = this.timeout()
     , start = new Date
     , ctx = this.ctx
     , finished
     , emitted;
 
-  if (ctx) ctx.runnable(this);
-
-  // timeout
-  if (this.async) {
-    if (ms) {
-      this.timer = setTimeout(function(){
-        done(new Error('timeout of ' + ms + 'ms exceeded'));
-        self.timedOut = true;
-      }, ms);
-    }
-  }
+  // Some times the ctx exists but it is not runnable
+  if (ctx && ctx.runnable) ctx.runnable(this);
 
   // called multiple times
   function multiple(err) {
     if (emitted) return;
     emitted = true;
-    self.emit('error', err || new Error('done() called multiple times'));
+    self.emit('error', err || new Error('done() called multiple times; stacktrace may be inaccurate'));
   }
 
   // finished
   function done(err) {
+    var ms = self.timeout();
     if (self.timedOut) return;
-    if (finished) return multiple(err);
+    if (finished) return multiple(err || self._trace);
     self.clearTimeout();
     self.duration = new Date - start;
     finished = true;
+    if (!err && self.duration > ms && self._enableTimeouts) err = new Error('timeout of ' + ms + 'ms exceeded');
     fn(err);
   }
 
   // for .resetTimeout()
   this.callback = done;
 
-  // async
+  // explicit async with `done` argument
   if (this.async) {
+    this.resetTimeout();
+
     try {
       this.fn.call(ctx, function(err){
         if (err instanceof Error || toString.call(err) === "[object Error]") return done(err);
-        if (null != err) return done(new Error('done() invoked with non-Error: ' + err));
+        if (null != err) {
+          if (Object.prototype.toString.call(err) === '[object Object]') {
+            return done(new Error('done() invoked with non-Error: ' + JSON.stringify(err)));
+          } else {
+            return done(new Error('done() invoked with non-Error: ' + err));
+          }
+        }
         done();
       });
     } catch (err) {
       done(err);
     }
     return;
   }
 
   if (this.asyncOnly) {
     return done(new Error('--async-only option in use without declaring `done()`'));
   }
 
-  // sync
+  // sync or promise-returning
   try {
-    if (!this.pending) this.fn.call(ctx);
-    this.duration = new Date - start;
-    fn();
+    if (this.pending) {
+      done();
+    } else {
+      callFn(this.fn);
+    }
   } catch (err) {
-    fn(err);
+    done(err);
+  }
+
+  function callFn(fn) {
+    var result = fn.call(ctx);
+    if (result && typeof result.then === 'function') {
+      self.resetTimeout();
+      result
+        .then(function() {
+          done()
+        },
+        function(reason) {
+          done(reason || new Error('Promise rejected with no or falsy reason'))
+        });
+    } else {
+      done();
+    }
   }
 };
 
 }); // module: runnable.js
 
 require.register("runner.js", function(module, exports, require){
 /**
  * Module dependencies.
@@ -4506,17 +4606,16 @@ Runner.prototype.globals = function(arr)
  * @api private
  */
 
 Runner.prototype.checkGlobals = function(test){
   if (this.ignoreLeaks) return;
   var ok = this._globals;
 
   var globals = this.globalProps();
-  var isNode = process.kill;
   var leaks;
 
   if (test) {
     ok = ok.concat(test._allowedGlobals || []);
   }
 
   if(this.prevGlobalsLength == globals.length) return;
   this.prevGlobalsLength = globals.length;
@@ -4878,22 +4977,35 @@ Runner.prototype.runSuite = function(sui
 /**
  * Handle uncaught exceptions.
  *
  * @param {Error} err
  * @api private
  */
 
 Runner.prototype.uncaught = function(err){
-  debug('uncaught exception %s', err.message);
+  if (err) {
+    debug('uncaught exception %s', err !== function () {
+      return this;
+    }.call(err) ? err : ( err.message || err ));
+  } else {
+    debug('uncaught undefined exception');
+    err = new Error('Caught undefined error, did you throw without specifying what?');
+  }
+  err.uncaught = true;
+
   var runnable = this.currentRunnable;
-  if (!runnable || 'failed' == runnable.state) return;
+  if (!runnable) return;
+
+  var wasAlreadyDone = runnable.state;
+  this.fail(runnable, err);
+
   runnable.clearTimeout();
-  err.uncaught = true;
-  this.fail(runnable, err);
+
+  if (wasAlreadyDone) return;
 
   // recover from test
   if ('test' == runnable.type) {
     this.emit('test end', runnable);
     this.hookUp('afterEach', this.next);
     return;
   }
 
@@ -4944,17 +5056,17 @@ Runner.prototype.run = function(fn){
  * Cleanly abort execution
  *
  * @return {Runner} for chaining
  * @api public
  */
 Runner.prototype.abort = function(){
   debug('aborting');
   this._abort = true;
-}
+};
 
 /**
  * Filter leaks with the given globals flagged as `ok`.
  *
  * @param {Array} ok
  * @param {Array} globals
  * @return {Array}
  * @api private
@@ -5008,17 +5120,16 @@ function filterLeaks(ok, globals) {
   }
 
   return [];
  }
 
 }); // module: runner.js
 
 require.register("suite.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var EventEmitter = require('browser/events').EventEmitter
   , debug = require('browser/debug')('mocha:suite')
   , milliseconds = require('./ms')
   , utils = require('./utils')
@@ -5056,28 +5167,31 @@ exports.create = function(parent, title)
  * Initialize a new `Suite` with the given
  * `title` and `ctx`.
  *
  * @param {String} title
  * @param {Context} ctx
  * @api private
  */
 
-function Suite(title, ctx) {
+function Suite(title, parentContext) {
   this.title = title;
-  this.ctx = ctx;
+  var context = function() {};
+  context.prototype = parentContext;
+  this.ctx = new context();
   this.suites = [];
   this.tests = [];
   this.pending = false;
   this._beforeEach = [];
   this._beforeAll = [];
   this._afterEach = [];
   this._afterAll = [];
   this.root = !title;
   this._timeout = 2000;
+  this._enableTimeouts = true;
   this._slow = 75;
   this._bail = false;
 }
 
 /**
  * Inherit from `EventEmitter.prototype`.
  */
 
@@ -5094,38 +5208,55 @@ Suite.prototype.constructor = Suite;
  * @api private
  */
 
 Suite.prototype.clone = function(){
   var suite = new Suite(this.title);
   debug('clone');
   suite.ctx = this.ctx;
   suite.timeout(this.timeout());
+  suite.enableTimeouts(this.enableTimeouts());
   suite.slow(this.slow());
   suite.bail(this.bail());
   return suite;
 };
 
 /**
  * Set timeout `ms` or short-hand such as "2s".
  *
  * @param {Number|String} ms
  * @return {Suite|Number} for chaining
  * @api private
  */
 
 Suite.prototype.timeout = function(ms){
   if (0 == arguments.length) return this._timeout;
+  if (ms === 0) this._enableTimeouts = false;
   if ('string' == typeof ms) ms = milliseconds(ms);
   debug('timeout %d', ms);
   this._timeout = parseInt(ms, 10);
   return this;
 };
 
 /**
+  * Set timeout `enabled`.
+  *
+  * @param {Boolean} enabled
+  * @return {Suite|Boolean} self or enabled
+  * @api private
+  */
+
+Suite.prototype.enableTimeouts = function(enabled){
+  if (arguments.length === 0) return this._enableTimeouts;
+  debug('enableTimeouts %s', enabled);
+  this._enableTimeouts = enabled;
+  return this;
+};
+
+/**
  * Set slow `ms` or short-hand such as "2s".
  *
  * @param {Number|String} ms
  * @return {Suite|Number} for chaining
  * @api private
  */
 
 Suite.prototype.slow = function(ms){
@@ -5154,81 +5285,109 @@ Suite.prototype.bail = function(bail){
 /**
  * Run `fn(test[, done])` before running tests.
  *
  * @param {Function} fn
  * @return {Suite} for chaining
  * @api private
  */
 
-Suite.prototype.beforeAll = function(fn){
+Suite.prototype.beforeAll = function(title, fn){
   if (this.pending) return this;
-  var hook = new Hook('"before all" hook', fn);
+  if ('function' === typeof title) {
+    fn = title;
+    title = fn.name;
+  }
+  title = '"before all" hook' + (title ? ': ' + title : '');
+
+  var hook = new Hook(title, fn);
   hook.parent = this;
   hook.timeout(this.timeout());
+  hook.enableTimeouts(this.enableTimeouts());
   hook.slow(this.slow());
   hook.ctx = this.ctx;
   this._beforeAll.push(hook);
   this.emit('beforeAll', hook);
   return this;
 };
 
 /**
  * Run `fn(test[, done])` after running tests.
  *
  * @param {Function} fn
  * @return {Suite} for chaining
  * @api private
  */
 
-Suite.prototype.afterAll = function(fn){
+Suite.prototype.afterAll = function(title, fn){
   if (this.pending) return this;
-  var hook = new Hook('"after all" hook', fn);
+  if ('function' === typeof title) {
+    fn = title;
+    title = fn.name;
+  }
+  title = '"after all" hook' + (title ? ': ' + title : '');
+
+  var hook = new Hook(title, fn);
   hook.parent = this;
   hook.timeout(this.timeout());
+  hook.enableTimeouts(this.enableTimeouts());
   hook.slow(this.slow());
   hook.ctx = this.ctx;
   this._afterAll.push(hook);
   this.emit('afterAll', hook);
   return this;
 };
 
 /**
  * Run `fn(test[, done])` before each test case.
  *
  * @param {Function} fn
  * @return {Suite} for chaining
  * @api private
  */
 
-Suite.prototype.beforeEach = function(fn){
+Suite.prototype.beforeEach = function(title, fn){
   if (this.pending) return this;
-  var hook = new Hook('"before each" hook', fn);
+  if ('function' === typeof title) {
+    fn = title;
+    title = fn.name;
+  }
+  title = '"before each" hook' + (title ? ': ' + title : '');
+
+  var hook = new Hook(title, fn);
   hook.parent = this;
   hook.timeout(this.timeout());
+  hook.enableTimeouts(this.enableTimeouts());
   hook.slow(this.slow());
   hook.ctx = this.ctx;
   this._beforeEach.push(hook);
   this.emit('beforeEach', hook);
   return this;
 };
 
 /**
  * Run `fn(test[, done])` after each test case.
  *
  * @param {Function} fn
  * @return {Suite} for chaining
  * @api private
  */
 
-Suite.prototype.afterEach = function(fn){
+Suite.prototype.afterEach = function(title, fn){
   if (this.pending) return this;
-  var hook = new Hook('"after each" hook', fn);
+  if ('function' === typeof title) {
+    fn = title;
+    title = fn.name;
+  }
+  title = '"after each" hook' + (title ? ': ' + title : '');
+
+  var hook = new Hook(title, fn);
   hook.parent = this;
   hook.timeout(this.timeout());
+  hook.enableTimeouts(this.enableTimeouts());
   hook.slow(this.slow());
   hook.ctx = this.ctx;
   this._afterEach.push(hook);
   this.emit('afterEach', hook);
   return this;
 };
 
 /**
@@ -5237,16 +5396,17 @@ Suite.prototype.afterEach = function(fn)
  * @param {Suite} suite
  * @return {Suite} for chaining
  * @api private
  */
 
 Suite.prototype.addSuite = function(suite){
   suite.parent = this;
   suite.timeout(this.timeout());
+  suite.enableTimeouts(this.enableTimeouts());
   suite.slow(this.slow());
   suite.bail(this.bail());
   this.suites.push(suite);
   this.emit('suite', suite);
   return this;
 };
 
 /**
@@ -5255,16 +5415,17 @@ Suite.prototype.addSuite = function(suit
  * @param {Test} test
  * @return {Suite} for chaining
  * @api private
  */
 
 Suite.prototype.addTest = function(test){
   test.parent = this;
   test.timeout(this.timeout());
+  test.enableTimeouts(this.enableTimeouts());
   test.slow(this.slow());
   test.ctx = this.ctx;
   this.tests.push(test);
   this.emit('test', test);
   return this;
 };
 
 /**
@@ -5312,17 +5473,16 @@ Suite.prototype.eachTest = function(fn){
     suite.eachTest(fn);
   });
   return this;
 };
 
 }); // module: suite.js
 
 require.register("test.js", function(module, exports, require){
-
 /**
  * Module dependencies.
  */
 
 var Runnable = require('./runnable');
 
 /**
  * Expose `Test`.
@@ -5358,16 +5518,19 @@ Test.prototype.constructor = Test;
 
 require.register("utils.js", function(module, exports, require){
 /**
  * Module dependencies.
  */
 
 var fs = require('browser/fs')
   , path = require('browser/path')
+  , basename = path.basename
+  , exists = fs.existsSync || path.existsSync
+  , glob = require('browser/glob')
   , join = path.join
   , debug = require('browser/debug')('mocha:watch');
 
 /**
  * Ignored directories.
  */
 
 var ignore = ['node_modules', '.git'];
@@ -5523,26 +5686,29 @@ function ignored(path){
 
 /**
  * Lookup files in the given `dir`.
  *
  * @return {Array}
  * @api private
  */
 
-exports.files = function(dir, ret){
+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, ret);
-    } else if (path.match(/\.(js|coffee|litcoffee|coffee.md)$/)) {
+      exports.files(path, ext, ret);
+    } else if (path.match(re)) {
       ret.push(path);
     }
   });
 
   return ret;
 };
 
 /**
@@ -5563,41 +5729,29 @@ exports.slug = function(str){
 /**
  * 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 *\(.*\) *{/, '')
+    .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '')
     .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);
 };
 
 /**
- * Escape regular expression characters in `str`.
- *
- * @param {String} str
- * @return {String}
- * @api private
- */
-
-exports.escapeRegexp = function(str){
-  return str.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
-};
-
-/**
  * Trim the given `str`.
  *
  * @param {String} str
  * @return {String}
  * @api private
  */
 
 exports.trim = function(str){
@@ -5634,37 +5788,132 @@ exports.parseQuery = function(qs){
 function highlight(js) {
   return js
     .replace(/</g, '&lt;')
     .replace(/>/g, '&gt;')
     .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
     .replace(/('.*?')/gm, '<span class="string">$1</span>')
     .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
     .replace(/(\d+)/gm, '<span class="number">$1</span>')
-    .replace(/\bnew *(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
+    .replace(/\bnew[ \t]+(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
     .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '<span class="keyword">$1</span>')
 }
 
 /**
  * Highlight the contents of tag `name`.
  *
  * @param {String} name
  * @api private
  */
 
 exports.highlightTags = function(name) {
-  var code = document.getElementsByTagName(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);
   }
 };
 
+
+/**
+ * Stringify `obj`.
+ *
+ * @param {Object} obj
+ * @return {String}
+ * @api private
+ */
+
+exports.stringify = function(obj) {
+  if (obj instanceof RegExp) return obj.toString();
+  return JSON.stringify(exports.canonicalize(obj), null, 2).replace(/,(\n|$)/g, '$1');
+};
+
+/**
+ * Return a new object that has the keys in sorted order.
+ * @param {Object} obj
+ * @param {Array} [stack]
+ * @return {Object}
+ * @api private
+ */
+
+exports.canonicalize = function(obj, stack) {
+  stack = stack || [];
+
+  if (exports.indexOf(stack, obj) !== -1) return '[Circular]';
+
+  var canonicalizedObj;
+
+  if ({}.toString.call(obj) === '[object Array]') {
+    stack.push(obj);
+    canonicalizedObj = exports.map(obj, function (item) {
+      return exports.canonicalize(item, stack);
+    });
+    stack.pop();
+  } else if (typeof obj === 'object' && obj !== null) {
+    stack.push(obj);
+    canonicalizedObj = {};
+    exports.forEach(exports.keys(obj).sort(), function (key) {
+      canonicalizedObj[key] = exports.canonicalize(obj[key], stack);
+    });
+    stack.pop();
+  } else {
+    canonicalizedObj = obj;
+  }
+
+  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;
+};
+
 }); // module: utils.js
 // The global object is "self" in Web Workers.
-global = (function() { return this; })();
+var global = (function() { return this; })();
 
 /**
  * Save timer references to avoid Sinon interfering (see GH-237).
  */
 
 var Date = global.Date;
 var setTimeout = global.setTimeout;
 var setInterval = global.setInterval;
@@ -5681,23 +5930,30 @@ var clearInterval = global.clearInterval
  */
 
 var process = {};
 process.exit = function(status){};
 process.stdout = {};
 
 var uncaughtExceptionHandlers = [];
 
+var originalOnerrorHandler = global.onerror;
+
 /**
  * Remove uncaughtException listener.
+ * Revert to original onerror handler if previously defined.
  */
 
 process.removeListener = function(e, fn){
   if ('uncaughtException' == e) {
-    global.onerror = function() {};
+    if (originalOnerrorHandler) {
+      global.onerror = originalOnerrorHandler;
+    } else {
+      global.onerror = function() {};
+    }
     var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn);
     if (i != -1) { uncaughtExceptionHandlers.splice(i, 1); }
   }
 };
 
 /**
  * Implements uncaughtException listener.
  */
@@ -5790,23 +6046,24 @@ mocha.setup = function(opts){
 mocha.run = function(fn){
   var options = mocha.options;
   mocha.globals('location');
 
   var query = Mocha.utils.parseQuery(global.location.search || '');
   if (query.grep) mocha.grep(query.grep);
   if (query.invert) mocha.invert();
 
-  return Mocha.prototype.run.call(mocha, function(){
+  return Mocha.prototype.run.call(mocha, function(err){
     // The DOM Document is not available in Web Workers.
-    if (global.document) {
+    var document = global.document;
+    if (document && document.getElementById('mocha') && options.noHighlighting !== true) {
       Mocha.utils.highlightTags('code');
     }
-    if (fn) fn();
+    if (fn) fn(err);
   });
 };
 
 /**
  * Expose the process shim.
  */
 
 Mocha.process = process;
-})();
\ No newline at end of file
+})();
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -1,33 +1,33 @@
 <!DOCTYPE html>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/.  -->
 <html>
 <head>
   <meta charset="utf-8">
   <title>Loop mocha tests</title>
-  <link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
+  <link rel="stylesheet" media="all" href="../shared/vendor/mocha-2.0.1.css">
 </head>
 <body>
   <div id="mocha">
     <p><a href="../">Index</a></p>
     <p><a href="../shared/">Shared Tests</a></p>
  </div>
   <div id="messages"></div>
   <div id="fixtures"></div>
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.11.2.js"></script>
   <script src="../../content/shared/libs/jquery-2.1.0.js"></script>
   <script src="../../content/shared/libs/lodash-2.4.1.js"></script>
   <script src="../../content/shared/libs/backbone-1.1.2.js"></script>
   <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
   <!-- test dependencies -->
-  <script src="../shared/vendor/mocha-1.17.1.js"></script>
+  <script src="../shared/vendor/mocha-2.0.1.js"></script>
   <script src="../shared/vendor/chai-1.9.0.js"></script>
   <script src="../shared/vendor/sinon-1.9.0.js"></script>
   <script src="../shared/sdk_mock.js"></script>
   <script>
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
   <!-- App scripts -->
--- a/browser/components/loop/test/standalone/multiplexGum_test.js
+++ b/browser/components/loop/test/standalone/multiplexGum_test.js
@@ -194,17 +194,17 @@ describe("loop.standaloneMedia._Multiple
           expect(error).to.eql(fakeError);
           expect(calls).to.eql(11);
           done();
         });
       });
 
     it("should not call a getPermsAndCacheMedia success callback at the time" +
        " of gUM success callback fires",
-      function(done) {
+      function() {
         var fakeLocalStream = {};
         multiplexGum.userMedia.localStream = fakeLocalStream;
         navigator.originalGum.callsArgWith(1, fakeLocalStream);
         var calledOnce = false;
         var promiseCalledOnce = new Promise(function(resolve, reject) {
 
           multiplexGum.getPermsAndCacheMedia(null,
             function gPACMSuccess(localStream) {
@@ -214,59 +214,55 @@ describe("loop.standaloneMedia._Multiple
               if (calledOnce) {
                 sinon.assert.fail("original callback was called twice");
               }
               calledOnce = true;
               resolve();
             }, function() {
               sinon.assert.fail("error callback should not have fired");
               reject();
-              done();
             });
         });
 
-        promiseCalledOnce.then(function() {
+        return promiseCalledOnce.then(function() {
           defaultGum(null, function gUMSuccess(localStream2) {
             expect(localStream2).to.eql(fakeLocalStream);
             expect(multiplexGum.userMedia).to.have.property('pending', false);
             expect(multiplexGum.userMedia.successCallbacks.length).to.equal(0);
-            done();
           });
         });
       });
 
     it("should not call a getPermsAndCacheMedia error callback when the " +
       " gUM error callback fires",
-      function(done) {
+      function() {
         var fakeError = "monkeys ate the stream";
         multiplexGum.userMedia.error = fakeError;
         navigator.originalGum.callsArgWith(2, fakeError);
         var calledOnce = false;
         var promiseCalledOnce = new Promise(function(resolve, reject) {
           multiplexGum.getPermsAndCacheMedia(null, function() {
             sinon.assert.fail("success callback should not have fired");
             reject();
-            done();
           }, function gPACMError(errString) {
             expect(errString).to.eql(fakeError);
             expect(multiplexGum.userMedia).to.have.property('pending', false);
             if (calledOnce) {
               sinon.assert.fail("original error callback was called twice");
             }
             calledOnce = true;
             resolve();
           });
         });
 
-        promiseCalledOnce.then(function() {
+        return promiseCalledOnce.then(function() {
           defaultGum(null, function() {},
             function gUMError(errString) {
               expect(errString).to.eql(fakeError);
               expect(multiplexGum.userMedia).to.have.property('pending', false);
-              done();
             });
         });
       });
 
     it("should call the success callback with a new stream, " +
        " when a new stream is available",
       function(done) {
         var endedStream = {ended: true};