Repository: mochajs/mocha

Problem statement:
Decouple the "root" property on Suite from title length.
Right now, Mocha deems a `Suite` to be a `root` if the title for it is empty, as can be seen [here](https://github.com/mochajs/mocha/blob/2bb2b9fa35818db7a02e5068364b0c417436b1af/lib/suite.js#L60):

```
function Suite (title, parentContext) {
  if (!utils.isString(title)) {
    throw new Error('Suite `title` should be a "string" but "' + typeof title + '" was given instead.');
  }
  this.title = title;
  // skipping non-pertinent stuff...
  this.root = !title;
```

Mocha creates a first `Suite` with `''` for title, which marks that suite as the root suite. This is a problem because, it seems to me when the user uses an empty string to name a suite, this should not result in Mocha building a structurally deficient tree of tests. (A tree with two "root" suites.)

Being able to name suites with the empty string (without at the same time marking them as "root") is useful. Sometimes I have suites where testing what is logically a single piece of functionality is best done by dividing the tests into two groups: simple tests that can be performed with super fast setup code, more complex tests that need a costlier setup. Oh, I could use the setup code for the 2nd group for all tests, but that would increase the total run time. I end up with something like:

```
// Testing the method foo on class Bar.
describe("#foo", () => {
  describe("", () => { 
    beforeEach(() => {
       // create a trivial data structure that is quick to create and sufficient for the tests in this group.
    });

    // tests...
  });

  describe("", () => { 
    beforeEach(() => {
       // create a complex structure that is costlier to create but needed for these tests.
    }); 

    // tests...
  });
});
```

The fact that the suite uses different setups for the two groups of tests is an internal detail that is not useful to know in test reports. The need for different test setups does not always correlate with divisions that are meaningful from the point of view of reporting successes or failures. It *often does*, but not always.

So I would suggest that the code be changed to determine the value of `root` through some other test than the length of the suite's title. There could be a unique object that serves as a marker to indicate "this suite I'm building is a root suite". It could be "statically" added to `Suite` (e.g. `Suite.Root` so doing `new Suite(Suite.Root, ...)` would result in a root suite).

Ultimately, though, the `root` property seems redundant to me: a `Suite` which has no `parent` is a root suite, no? So "root"-ness should correlate with the absence of a set `parent`. But maybe there's some scenario I'm missing? At any rate, removing `root` cannot be done without breakage. The `karma-mocha` plugin, for instance, relies on it to produce reports (which is how I discovered the problem).




Hints:

Patch:
diff --git a/lib/mocha.js b/lib/mocha.js
--- a/lib/mocha.js
+++ b/lib/mocha.js
@@ -92,7 +92,7 @@ function Mocha(options) {
   this.files = [];
   this.options = options;
   // root suite
-  this.suite = new exports.Suite('', new exports.Context());
+  this.suite = new exports.Suite('', new exports.Context(), true);
 
   if ('useColors' in options) {
     utils.deprecate(
diff --git a/lib/suite.js b/lib/suite.js
--- a/lib/suite.js
+++ b/lib/suite.js
@@ -41,15 +41,18 @@ exports.create = function(parent, title) {
 };
 
 /**
- * Initialize a new `Suite` with the given `title` and `ctx`. Derived from [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
+ * Constructs a new `Suite` instance with the given `title`, `ctx`, and `isRoot`.
  *
- * @memberof Mocha
  * @public
  * @class
- * @param {string} title
- * @param {Context} parentContext
+ * @extends EventEmitter
+ * @memberof Mocha
+ * @see {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter}
+ * @param {string} title - Suite title.
+ * @param {Context} parentContext - Parent context instance.
+ * @param {boolean} [isRoot=false] - Whether this is the root suite.
  */
-function Suite(title, parentContext) {
+function Suite(title, parentContext, isRoot) {
   if (!utils.isString(title)) {
     throw createInvalidArgumentTypeError(
       'Suite argument "title" must be a string. Received type "' +
@@ -70,7 +73,7 @@ function Suite(title, parentContext) {
   this._beforeAll = [];
   this._afterEach = [];
   this._afterAll = [];
-  this.root = !title;
+  this.root = isRoot === true;
   this._timeout = 2000;
   this._enableTimeouts = true;
   this._slow = 75;
@@ -326,6 +329,7 @@ Suite.prototype.afterEach = function(title, fn) {
  */
 Suite.prototype.addSuite = function(suite) {
   suite.parent = this;
+  suite.root = false;
   suite.timeout(this.timeout());
   suite.retries(this.retries());
   suite.enableTimeouts(this.enableTimeouts());


Test patch:
diff --git a/test/unit/suite.spec.js b/test/unit/suite.spec.js
--- a/test/unit/suite.spec.js
+++ b/test/unit/suite.spec.js
@@ -302,6 +302,27 @@ describe('Suite', function() {
     });
   });
 
+  describe('.create()', function() {
+    before(function() {
+      this.first = new Suite('Root suite', {}, true);
+      this.second = new Suite('RottenRoot suite', {}, true);
+      this.first.addSuite(this.second);
+    });
+
+    it('does not create a second root suite', function() {
+      expect(this.second.parent, 'to be', this.first);
+      expect(this.first.root, 'to be', true);
+      expect(this.second.root, 'to be', false);
+    });
+
+    it('does not denote the root suite by being titleless', function() {
+      var emptyTitleSuite = Suite.create(this.second, '');
+      expect(emptyTitleSuite.parent, 'to be', this.second);
+      expect(emptyTitleSuite.root, 'to be', false);
+      expect(this.second.root, 'to be', false);
+    });
+  });
+
   describe('.addSuite()', function() {
     beforeEach(function() {
       this.first = new Suite('First suite');
@@ -390,8 +411,8 @@ describe('Suite', function() {
     describe('when there is a parent', function() {
       describe('the parent is the root suite', function() {
         it('returns the suite title', function() {
-          var parentSuite = new Suite('');
-          parentSuite.addSuite(this.suite);
+          var rootSuite = new Suite('', {}, true);
+          rootSuite.addSuite(this.suite);
           expect(this.suite.titlePath(), 'to equal', ['A Suite']);
         });
       });