Repository: mochajs/mocha

Problem statement:
Opera reports 1 failure when actually everything passed
I have a test case that has exactly 41 tests and while testing in node or Chrome/Safari/Firefox everything works great except in Opera.

It says:

```
passes: 41
failures: 1
duration: 0.21s
```

In chrome (for example) it says:

```
passes: 41
failures: 0
duration: 0.31s
```

This can be reproduced by checking out https://github.com/TrinityJS/Classify and opening test/Classify.all.html in Opera.



Hints:
+1, same issue in paulmillr/es6-shim

+1, same issue in Chrome on one of my projects.  I find it occurs after the very first test operation, incrementing both the passes and failures by one, no matter what the first test operation is.

hmm interesting I'll take a look

k yeah i get the same, nice that opera finally has a decent console

apparently we have a "stats" global that's not being reported, it's getting the "test end" event but `.passed` is true even though it failed, some state issue

+1

Now in opera an error is thrown:

global leak detected: stats

Thats exactly what @visionmedia  said, mocha has a global leak :p

This issue has been inactive for over 1 month so I'm closing it. If you think it's still an issue re-open. - tjbot

@visionmedia the problem still persists.. but I don't have permission to reopen..

I have the same issue and I temporarily resolved using this code:

``` js
var globals = [];
if (navigator.userAgent.indexOf('Opera') !== -1) {
    globals.push('stats', 'report');
}

mocha
.run()
.globals(globals);
```

Still this should be fixed in mocha itself.

Bump!

From chaplinjs/chaplin#147:

<blockquote>I get strange <code>global leaks detected: stats, report</code> errors in Opera. We’re definitely not creating these vars, this must be an error in Mocha.</blockquote>

I just got the same. Working fine in Firefox 15 and Chrome 22, but causing these global detected failures in **the default Mocha test suite, both in Opera 12 and IE9**.

So I booted up BrowserStack and looked up in the console what these variables where actually set to:
![Mocha Standard tests fail in Opera and IE](https://s3.amazonaws.com/f.cl.ly/items/1a2s18290O3K2o3J3U2S/Mocha%20Standard%20failure.png)

Turns out these are the DOM elements Mocha creates during the test. And guess what, both Opera and IE are famous for exposing global variables for every element ID.

The way we fixed this in QUnit (jquery/qunit#212) is by prefixing our IDs with "qunit-" (never a bad habit, but with "mocha-", of course). And filtering these out in the global check ([jquery/qunit@`1e21c01e`](https://github.com/jquery/qunit/commit/1e21c01eed6ace2188d1a57a741a76a36f4233ce)).

hahaha yikes, that's messed, thanks for looking into it

I'm preparing a pull request now.


Patch:
diff --git a/lib/reporters/html.js b/lib/reporters/html.js
--- a/lib/reporters/html.js
+++ b/lib/reporters/html.js
@@ -28,7 +28,7 @@ exports = module.exports = HTML;
  * Stats template.
  */
 
-var statsTemplate = '<ul id="stats">'
+var statsTemplate = '<ul id="mocha-stats">'
   + '<li class="progress"><canvas width="40" height="40"></canvas></li>'
   + '<li class="passes"><a href="#">passes:</a> <em>0</em></li>'
   + '<li class="failures"><a href="#">failures:</a> <em>0</em></li>'
@@ -56,7 +56,7 @@ function HTML(runner, root) {
     , failuresLink = items[2].getElementsByTagName('a')[0]
     , duration = items[3].getElementsByTagName('em')[0]
     , canvas = stat.getElementsByTagName('canvas')[0]
-    , report = fragment('<ul id="report"></ul>')
+    , report = fragment('<ul id="mocha-report"></ul>')
     , stack = [report]
     , progress
     , ctx
@@ -183,7 +183,7 @@ function HTML(runner, root) {
  */
 
 function error(msg) {
-  document.body.appendChild(fragment('<div id="error">%s</div>', msg));
+  document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
 }
 
 /**
diff --git a/lib/runner.js b/lib/runner.js
--- a/lib/runner.js
+++ b/lib/runner.js
@@ -523,6 +523,8 @@ function filterLeaks(ok, globals) {
   return filter(globals, function(key){
     var matched = filter(ok, function(ok){
       if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]);
+      // Opera and IE expose global variables for HTML element IDs (issue #243)
+      if (/^mocha-/.test(key)) return true;
       return key == ok;
     });
     return matched.length == 0 && (!global.navigator || 'onerror' !== key);
diff --git a/mocha.js b/mocha.js
--- a/mocha.js
+++ b/mocha.js
@@ -937,6 +937,14 @@ module.exports = function(suite){
       var test = context.test(title, fn);
       mocha.grep(test.fullTitle());
     };
+
+    /**
+     * Pending test case.
+     */
+
+    context.test.skip = function(title){
+      context.test(title);
+    };
   });
 };
 
@@ -1211,6 +1219,18 @@ Mocha.prototype.slow = function(slow){
   return this;
 };
 
+/**
+ * Makes all tests async (accepting a callback)
+ *
+ * @return {Mocha}
+ * @api public
+ */
+
+Mocha.prototype.asyncOnly = function(){
+  this.options.asyncOnly = true;
+  return this;
+};
+
 /**
  * Run tests and invoke `fn()` when complete.
  *
@@ -1226,6 +1246,7 @@ Mocha.prototype.run = function(fn){
   var runner = new exports.Runner(suite);
   var reporter = new this._reporter(runner);
   runner.ignoreLeaks = 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);
@@ -1306,14 +1327,14 @@ function parse(str) {
  */
 
 function format(ms) {
-  if (ms == d) return (ms / d) + ' day';
-  if (ms > d) return (ms / d) + ' days';
-  if (ms == h) return (ms / h) + ' hour';
-  if (ms > h) return (ms / h) + ' hours';
-  if (ms == m) return (ms / m) + ' minute';
-  if (ms > m) return (ms / m) + ' minutes';
-  if (ms == s) return (ms / s) + ' second';
-  if (ms > s) return (ms / s) + ' seconds';
+  if (ms == d) return Math.round(ms / d) + ' day';
+  if (ms > d) return Math.round(ms / d) + ' days';
+  if (ms == h) return Math.round(ms / h) + ' hour';
+  if (ms > h) return Math.round(ms / h) + ' hours';
+  if (ms == m) return Math.round(ms / m) + ' minute';
+  if (ms > m) return Math.round(ms / m) + ' minutes';
+  if (ms == s) return Math.round(ms / s) + ' second';
+  if (ms > s) return Math.round(ms / s) + ' seconds';
   return ms + ' ms';
 }
 }); // module: ms.js
@@ -1899,7 +1920,7 @@ exports = module.exports = HTML;
  * Stats template.
  */
 
-var statsTemplate = '<ul id="stats">'
+var statsTemplate = '<ul id="mocha-stats">'
   + '<li class="progress"><canvas width="40" height="40"></canvas></li>'
   + '<li class="passes"><a href="#">passes:</a> <em>0</em></li>'
   + '<li class="failures"><a href="#">failures:</a> <em>0</em></li>'
@@ -1927,7 +1948,7 @@ function HTML(runner, root) {
     , failuresLink = items[2].getElementsByTagName('a')[0]
     , duration = items[3].getElementsByTagName('em')[0]
     , canvas = stat.getElementsByTagName('canvas')[0]
-    , report = fragment('<ul id="report"></ul>')
+    , report = fragment('<ul id="mocha-report"></ul>')
     , stack = [report]
     , progress
     , ctx
@@ -1994,7 +2015,7 @@ function HTML(runner, root) {
     window.scrollTo(0, document.body.scrollHeight);
 
     // TODO: add to stats
-    var percent = stats.tests / total * 100 | 0;
+    var percent = stats.tests / this.total * 100 | 0;
     if (progress) progress.update(percent).draw(ctx);
 
     // update stats
@@ -2054,7 +2075,7 @@ function HTML(runner, root) {
  */
 
 function error(msg) {
-  document.body.appendChild(fragment('<div id="error">%s</div>', msg));
+  document.body.appendChild(fragment('<div id="mocha-error">%s</div>', msg));
 }
 
 /**
@@ -2682,7 +2703,7 @@ function Markdown(runner) {
   runner.on('suite', function(suite){
     ++level;
     var slug = utils.slug(suite.fullTitle());
-    buf += '<a name="' + slug + '" />' + '\n';
+    buf += '<a name="' + slug + '"></a>' + '\n';
     buf += title(suite.title) + '\n';
   });
 
@@ -3658,7 +3679,11 @@ Runnable.prototype.run = function(fn){
     }
     return;
   }
-  
+
+  if (this.asyncOnly) {
+    return done(new Error('--async-only option in use without declaring `done()`'));
+  }
+
   // sync
   try {
     if (!this.pending) this.fn.call(ctx);
@@ -3685,6 +3710,19 @@ var EventEmitter = require('browser/events').EventEmitter
   , keys = utils.keys
   , noop = function(){};
 
+/**
+ * Non-enumerable globals.
+ */
+
+var globals = [
+  'setTimeout',
+  'clearTimeout',
+  'setInterval',
+  'clearInterval',
+  'XMLHttpRequest',
+  'Date'
+];
+
 /**
  * Expose `Runner`.
  */
@@ -3719,7 +3757,7 @@ function Runner(suite) {
   this.on('test end', function(test){ self.checkGlobals(test); });
   this.on('hook end', function(hook){ self.checkGlobals(hook); });
   this.grep(/.*/);
-  this.globals(utils.keys(global).concat(['errno']));
+  this.globals(this.globalProps().concat(['errno']));
 }
 
 /**
@@ -3770,6 +3808,25 @@ Runner.prototype.grepTotal = function(suite) {
   return total;
 };
 
+/**
+ * Return a list of global properties.
+ *
+ * @return {Array}
+ * @api private
+ */
+
+Runner.prototype.globalProps = function() {
+  var props = utils.keys(global);
+
+  // non-enumerables
+  for (var i = 0; i < globals.length; ++i) {
+    if (~props.indexOf(globals[i])) continue;
+    props.push(globals[i]);
+  }
+
+  return props;
+};
+
 /**
  * Allow the given `arr` of globals.
  *
@@ -3796,7 +3853,7 @@ Runner.prototype.globals = function(arr){
 Runner.prototype.checkGlobals = function(test){
   if (this.ignoreLeaks) return;
   var ok = this._globals;
-  var globals = keys(global);
+  var globals = this.globalProps();
   var isNode = process.kill;
   var leaks;
 
@@ -3976,6 +4033,8 @@ Runner.prototype.runTest = function(fn){
   var test = this.test
     , self = this;
 
+  if (this.asyncOnly) test.asyncOnly = true;
+
   try {
     test.on('error', function(err){
       self.fail(test, err);
@@ -4165,6 +4224,8 @@ function filterLeaks(ok, globals) {
   return filter(globals, function(key){
     var matched = filter(ok, function(ok){
       if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]);
+      // Opera and IE expose global variables for HTML element IDs (issue #243)
+      if (/^mocha-/.test(key)) return true;
       return key == ok;
     });
     return matched.length == 0 && (!global.navigator || 'onerror' !== key);
@@ -4901,6 +4962,7 @@ process.on = function(e, fn){
 
     var query = Mocha.utils.parseQuery(window.location.search || '');
     if (query.grep) mocha.grep(query.grep);
+    if (query.invert) mocha.invert();
 
     return Mocha.prototype.run.call(mocha, function(){
       Mocha.utils.highlightTags('code');


Test patch:
diff --git a/test/acceptance/globals.js b/test/acceptance/globals.js
--- a/test/acceptance/globals.js
+++ b/test/acceptance/globals.js
@@ -27,7 +27,13 @@ describe('global leaks', function(){
   it('should pass with wildcard', function(){
     global.callback123 = 'foo';
     global.callback345 = 'bar';
-  })
+  });
+
+  it('should pass when prefixed "mocha-"', function(){
+    // Opera and IE do this for HTML element IDs anyway
+    // but to sure we can assert this in any browser, simulate it.
+    global['mocha-example'] = { nodeType: 1 };
+  });
 
   afterEach(function(){
     // uncomment to test