Repository: testem/testem (js)

Issue:
Request: add QUnit.todo support to XUnit reporter
PR #1400 did excellent work supporting [QUnit.todo()](https://api.qunitjs.com/QUnit/test.todo/) support but they missed the [XUnit reporter](https://github.com/testem/testem/blob/master/lib/reporters/xunit_reporter.js).

Requesting this new feature. Thank you.

Pull request description:
Add support for `QUnit.todo` to XUnit reporter
Following #1400 , this PR completes it and adds support for [QUnit.todo](https://api.qunitjs.com/QUnit/test.todo/) to the XUnit reporter.

Resolves #1538 .


Patch:
diff --git a/lib/reporters/xunit_reporter.js b/lib/reporters/xunit_reporter.js
index b77ca0d..b64d037 100644
--- a/lib/reporters/xunit_reporter.js
+++ b/lib/reporters/xunit_reporter.js
@@ -3,7 +3,7 @@
 var XmlDom = require('@xmldom/xmldom');
 var indent = require('../utils/strutils').indent;
 
-class XUnitReporter {
+module.exports = class XUnitReporter {
   constructor(silent, out, config) {
     this.out = out || process.stdout;
     this.excludeStackTraces = config.get('xunit_exclude_stack');
@@ -13,6 +13,7 @@ class XUnitReporter {
     this.total = 0;
     this.pass = 0;
     this.skipped = 0;
+    this.todo = 0;
     this.results = [];
     this.startTime = new Date();
     this.endTime = null;
@@ -23,12 +24,15 @@ class XUnitReporter {
       launcher: prefix,
       result: data
     });
-    this.display(prefix, data);
+    this.display();
     this.total++;
+
     if (data.skipped) {
       this.skipped++;
-    } else if (data.passed) {
+    } else if (data.passed && !data.todo) {
       this.pass++;
+    } else if (!data.passed && data.todo) {
+      this.todo++;
     }
   }
 
@@ -46,11 +50,12 @@ class XUnitReporter {
 
     var rootNode = doc.documentElement;
     rootNode.setAttribute('name', 'Testem Tests');
-    rootNode.setAttribute('tests', this.total);
-    rootNode.setAttribute('skipped', this.skipped);
-    rootNode.setAttribute('failures', this.failures());
-    rootNode.setAttribute('timestamp', new Date());
-    rootNode.setAttribute('time', this.duration());
+    rootNode.setAttribute('tests', `${this.total}`);
+    rootNode.setAttribute('skipped', `${this.skipped}`);
+    rootNode.setAttribute('todo', `${this.todo}`);
+    rootNode.setAttribute('failures', `${this.failures()}`);
+    rootNode.setAttribute('timestamp', new Date().toString());
+    rootNode.setAttribute('time', `${this.duration() }`);
 
     for (var i = 0, len = this.results.length; i < len; i++) {
       var testcaseNode = this.getTestResultNode(doc, this.results[i]);
@@ -107,6 +112,9 @@ class XUnitReporter {
     } else if (result.skipped) {
       var skippedNode = document.createElement('skipped');
       resultNode.appendChild(skippedNode);
+    } else if (result.todo) {
+      var todoNode = document.createElement('todo');
+      resultNode.appendChild(todoNode);
     } else if (!result.passed) {
       var failureNode = document.createElement('failure');
       resultNode.appendChild(failureNode);
@@ -116,11 +124,14 @@ class XUnitReporter {
   }
 
   failures() {
-    return this.total - this.pass - this.skipped;
+    return this.total - this.pass - this.skipped - this.todo;
   }
 
   duration() {
-    return this._durationFromMs(this.endTime - this.startTime);
+    const endTime = this.endTime ? this.endTime.getTime() : 0;
+    const startTime = this.startTime.getTime();
+
+    return this._durationFromMs(endTime - startTime);
   }
 
   _durationFromMs(ms) {
@@ -132,6 +143,4 @@ class XUnitReporter {
       return 0;
     }
   }
-}
-
-module.exports = XUnitReporter;
+};


Test patch:
diff --git a/tests/ci/reporter_tests.js b/tests/ci/reporter_tests.js
index 42c73e1..680802c 100644
--- a/tests/ci/reporter_tests.js
+++ b/tests/ci/reporter_tests.js
@@ -798,7 +798,7 @@ describe('test reporters', function() {
       });
       reporter.finish();
       var output = stream.read().toString();
-      assert.match(output, /<testsuite name="Testem Tests" tests="1" skipped="0" failures="0" timestamp="(.+)" time="(\d+(\.\d+)?)">/);
+      assert.match(output, /<testsuite name="Testem Tests" tests="1" skipped="0" todo="0" failures="0" timestamp="(.+)" time="(\d+(\.\d+)?)">/);
       assert.match(output, /<testcase classname="phantomjs" name="it does &lt;cool> &quot;cool&quot; 'cool' stuff"/);
 
       assertXmlIsValid(output);
@@ -942,6 +942,20 @@ describe('test reporters', function() {
       assertXmlIsValid(output);
     });
 
+    it('outputs todo tests', function() {
+      var reporter = new XUnitReporter(false, stream, config);
+      reporter.report('phantomjs', {
+        name: 'it didnt work',
+        passed: false,
+        todo: true
+      });
+      reporter.finish();
+      var output = stream.read().toString();
+      assert.match(output, /<todo\/>/);
+
+      assertXmlIsValid(output);
+    });
+
     it('skipped tests are not considered failures', function() {
       var reporter = new XUnitReporter(false, stream, config);
       reporter.report('phantomjs', {
@@ -956,6 +970,20 @@ describe('test reporters', function() {
       assertXmlIsValid(output);
     });
 
+    it('passing todo tests are considered failures', function() {
+      var reporter = new XUnitReporter(false, stream, config);
+      reporter.report('phantomjs', {
+        name: 'it didnt work',
+        passed: true,
+        todo: true
+      });
+      reporter.finish();
+      var output = stream.read().toString();
+      assert.notMatch(output, /<failure/);
+
+      assertXmlIsValid(output);
+    });
+
     it('outputs failed tests', function() {
       var reporter = new XUnitReporter(false, stream, config);
       reporter.report('phantomjs', {