Repository: lo1tuma/eslint-plugin-mocha
Pull request #126 (2017-03-15 22:28:32)

Add settings to enable support for mocha.parallel
This PR adds a new shared settings value which accepts an array of additional suite names, for example:

`
  "settings": {
      "mocha/additionalSuiteNames': [ 'foo', 'bar' ],
  }
`

This is useful when working with modules like [mocha.parallel](https://github.com/danielstjules/mocha.parallel)

@lo1tuma let me know what you think and I'll adapt it for the following rules as well.

- [x] `no-top-level-hooks`
- [x] `max-top-level-suites`
- [x] `no-hooks-for-single-case`
- [x] `no-identical-title`
- [x] `no-nested-tests`
- [x] `no-sibling-hooks`

lo1tuma: Thanks for the PR.

Actually I was thinking about a more generic approach. I would prefer a setting which accepts an array of additional suite names, for example:

```
"settings": { "mocha/additionalSuiteNames": [ "parallel", "xparallel", "myOtherMochaExtensionCustomSuiteName" ] } }
```

This way we would be agnostic to any specific mocha extension and we don’t have to add a separate setting for each custom suite name. What do you think?
stefanbuck: Yeah, make sense. I've updated the PR as you suggested. Let me know if it's okay for you and I'll tweak the other rules as well.

Patch:
diff --git a/lib/rules/max-top-level-suites.js b/lib/rules/max-top-level-suites.js
index 94cc53c..b05407b 100644
--- a/lib/rules/max-top-level-suites.js
+++ b/lib/rules/max-top-level-suites.js
@@ -7,12 +7,14 @@
 
 var R = require('ramda'),
     astUtil = require('../util/ast'),
+    additionalSuiteNames = require('../util/settings').additionalSuiteNames,
     defaultSuiteLimit = 1;
 
 module.exports = function (context) {
     var stack = [],
         topLevelDescribes = [],
         options = context.options[0] || {},
+        settings = context.settings,
         suiteLimit;
 
     if (R.isNil(options.limit)) {
@@ -23,13 +25,13 @@ module.exports = function (context) {
 
     return {
         CallExpression: function (node) {
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
                 stack.push(node);
             }
         },
 
         'CallExpression:exit': function (node) {
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
                 if (stack.length === 1) {
                     topLevelDescribes.push(node);
                 }
diff --git a/lib/rules/no-hooks-for-single-case.js b/lib/rules/no-hooks-for-single-case.js
index 7b78c28..5a008ef 100644
--- a/lib/rules/no-hooks-for-single-case.js
+++ b/lib/rules/no-hooks-for-single-case.js
@@ -1,6 +1,7 @@
 'use strict';
 
-var astUtil = require('../util/ast');
+var astUtil = require('../util/ast'),
+    additionalSuiteNames = require('../util/settings').additionalSuiteNames;
 
 function newDescribeLayer(describeNode) {
     return {
@@ -13,6 +14,7 @@ function newDescribeLayer(describeNode) {
 module.exports = function (context) {
     var options = context.options[0] || {},
         allowedHooks = options.allow || [],
+        settings = context.settings,
         layers = [];
 
     function popLayer(node) {
@@ -40,7 +42,7 @@ module.exports = function (context) {
         },
 
         CallExpression: function (node) {
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
                 layers[layers.length - 1].testCount += 1;
                 layers.push(newDescribeLayer(node));
                 return;
diff --git a/lib/rules/no-identical-title.js b/lib/rules/no-identical-title.js
index 10137ab..137ceab 100644
--- a/lib/rules/no-identical-title.js
+++ b/lib/rules/no-identical-title.js
@@ -1,6 +1,7 @@
 'use strict';
 
-var astUtil = require('../util/ast');
+var astUtil = require('../util/ast'),
+    additionalSuiteNames = require('../util/settings').additionalSuiteNames;
 
 function newLayer() {
     return {
@@ -22,7 +23,9 @@ function handlTestCaseTitles(context, titles, node, title) {
 }
 
 function handlTestSuiteTitles(context, titles, node, title) {
-    if (!astUtil.isDescribe(node)) {
+    var settings = context.settings;
+
+    if (!astUtil.isDescribe(node, additionalSuiteNames(settings))) {
         return;
     }
     if (titles.indexOf(title) !== -1) {
@@ -41,12 +44,14 @@ function isFirstArgLiteral(node) {
 module.exports = function (context) {
     var titleLayers = [
         newLayer()
-    ];
+    ],
+    settings = context.settings;
+
     return {
         CallExpression: function (node) {
             var currentLayer = titleLayers[titleLayers.length - 1],
                 title;
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
                 titleLayers.push(newLayer());
             }
             if (!isFirstArgLiteral(node)) {
@@ -58,7 +63,7 @@ module.exports = function (context) {
             handlTestSuiteTitles(context, currentLayer.describeTitles, node, title);
         },
         'CallExpression:exit': function (node) {
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
                 titleLayers.pop();
             }
         }
diff --git a/lib/rules/no-sibling-hooks.js b/lib/rules/no-sibling-hooks.js
index 4270a63..700d82f 100644
--- a/lib/rules/no-sibling-hooks.js
+++ b/lib/rules/no-sibling-hooks.js
@@ -1,6 +1,7 @@
 'use strict';
 
-var astUtil = require('../util/ast');
+var astUtil = require('../util/ast'),
+    additionalSuiteNames = require('../util/settings').additionalSuiteNames;
 
 function newDescribeLayer(describeNode) {
     return {
@@ -13,7 +14,8 @@ function newDescribeLayer(describeNode) {
 }
 
 module.exports = function (context) {
-    var isUsed = [];
+    var isUsed = [],
+        settings = context.settings;
 
     return {
         Program: function (node) {
@@ -22,7 +24,7 @@ module.exports = function (context) {
 
         CallExpression: function (node) {
             var name = node.callee && node.callee.name;
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
               isUsed.push(newDescribeLayer(node));
               return;
             }
diff --git a/lib/rules/no-top-level-hooks.js b/lib/rules/no-top-level-hooks.js
index 83f36f4..bc8ac2b 100644
--- a/lib/rules/no-top-level-hooks.js
+++ b/lib/rules/no-top-level-hooks.js
@@ -1,13 +1,15 @@
 'use strict';
 
-var astUtil = require('../util/ast');
+var astUtil = require('../util/ast'),
+    additionalSuiteNames = require('../util/settings').additionalSuiteNames;
 
 module.exports = function (context) {
-    var testSuiteStack = [];
+    var settings = context.settings,
+        testSuiteStack = [];
 
     return {
         CallExpression: function (node) {
-            if (astUtil.isDescribe(node)) {
+            if (astUtil.isDescribe(node, additionalSuiteNames(settings))) {
                 testSuiteStack.push(node);
                 return;
             }
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 471673c..176adf3 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -21,10 +21,10 @@ function getNodeName(node) {
     return node.name;
 }
 
-function isDescribe(node) {
+function isDescribe(node, additionalSuiteNames) {
   return node
       && node.type === 'CallExpression'
-      && describeAliases.indexOf(getNodeName(node.callee)) > -1;
+      && describeAliases.concat(additionalSuiteNames).indexOf(getNodeName(node.callee)) > -1;
 }
 
 function isHookIdentifier(node) {
diff --git a/lib/util/settings.js b/lib/util/settings.js
index 749d287..af239cc 100644
--- a/lib/util/settings.js
+++ b/lib/util/settings.js
@@ -13,5 +13,6 @@ function settingFor(propertyName) {
 
 module.exports = {
   getAdditionalTestFunctions: settingFor('additionalTestFunctions'),
+  additionalSuiteNames: settingFor('additionalSuiteNames'),
   getAdditionalXFunctions: settingFor('additionalXFunctions')
 };


Test patch:
diff --git a/lib/rules/no-nested-tests.js b/lib/rules/no-nested-tests.js
index 84fddef..d7fe8b7 100644
--- a/lib/rules/no-nested-tests.js
+++ b/lib/rules/no-nested-tests.js
@@ -1,9 +1,11 @@
 'use strict';
 
-var astUtils = require('../util/ast');
+var astUtils = require('../util/ast'),
+    additionalSuiteNames = require('../util/settings').additionalSuiteNames;
 
 module.exports = function noNestedTests(context) {
-    var testNestingLevel = 0;
+    var testNestingLevel = 0,
+        settings = context.settings;
 
     function report(callExpression, isTestCase) {
         var message = isTestCase ? 'Unexpected test nested within another test.' :
@@ -18,7 +20,7 @@ module.exports = function noNestedTests(context) {
     return {
         CallExpression: function (node) {
             var isTestCase = astUtils.isTestCase(node),
-                isDescribe = astUtils.isDescribe(node);
+                isDescribe = astUtils.isDescribe(node, additionalSuiteNames(settings));
 
             if (testNestingLevel > 0 && (isTestCase || isDescribe)) {
                 report(node, isTestCase);
diff --git a/test/rules/max-top-level-suites.js b/test/rules/max-top-level-suites.js
index d4a0efc..bd558c9 100644
--- a/test/rules/max-top-level-suites.js
+++ b/test/rules/max-top-level-suites.js
@@ -40,6 +40,19 @@ ruleTester.run('max-top-level-suites', rules['max-top-level-suites'], {
             options: [ { } ],
             code: 'someOtherFunction();'
         },
+        {
+            code: 'foo("This is a test", function () { });',
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            }
+        }, {
+            code: 'foo("This is a test", function () { });',
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           }
+        },
         'someOtherFunction();'
     ],
 
@@ -151,6 +164,27 @@ ruleTester.run('max-top-level-suites', rules['max-top-level-suites'], {
             errors: [
                 { message: 'The number of top-level suites is more than 1.' }
             ]
+        },
+        {
+            code: 'foo("this is a test", function () { });' +
+                  'foo("this is a different test", function () { });',
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            },
+            errors: [
+                { message: 'The number of top-level suites is more than 1.' }
+            ]
+        }, {
+            code: 'foo("this is a test", function () { });' +
+                  'foo("this is a different test", function () { });',
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           },
+           errors: [
+               { message: 'The number of top-level suites is more than 1.' }
+           ]
         }
     ]
 });
diff --git a/test/rules/no-hooks-for-single-case.js b/test/rules/no-hooks-for-single-case.js
index 6ba8943..07dbe36 100644
--- a/test/rules/no-hooks-for-single-case.js
+++ b/test/rules/no-hooks-for-single-case.js
@@ -128,6 +128,32 @@ ruleTester.run('no-hooks-for-single-case', rules['no-hooks-for-single-case'], {
                 '});'
             ].join('\n'),
             options: [ { allow: [ 'after', 'afterEach' ] } ]
+        },
+        {
+            code: [
+                'foo(function() {',
+                '    before(function() {});',
+                '    it(function() {});',
+                '    it(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+            }
+        },
+        {
+            code: [
+                'foo(function() {',
+                '    before(function() {});',
+                '    it(function() {});',
+                '    it(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            }
         }
     ],
 
@@ -258,6 +284,30 @@ ruleTester.run('no-hooks-for-single-case', rules['no-hooks-for-single-case'], {
             ].join('\n'),
             options: [ { allow: [ 'before' ] } ],
             errors: [ { message: 'Unexpected use of Mocha `after` hook for a single test case', column: 5, line: 2 } ]
+        },
+        {
+            code: [
+                'foo(function() {',
+                '    before(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+            },
+            errors: [ { message: 'Unexpected use of Mocha `before` hook for a single test case', column: 5, line: 2 } ]
+        },
+        {
+            code: [
+                'foo(function() {',
+                '    before(function() {});',
+                '});'
+            ].join('\n'),
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            },
+            errors: [ { message: 'Unexpected use of Mocha `before` hook for a single test case', column: 5, line: 2 } ]
         }
     ]
 
diff --git a/test/rules/no-identical-title.js b/test/rules/no-identical-title.js
index 9b956b9..835fc4f 100644
--- a/test/rules/no-identical-title.js
+++ b/test/rules/no-identical-title.js
@@ -91,7 +91,26 @@ ruleTester.run('no-identical-title', rules['no-identical-title'], {
             '        describe("describe2", function() {});',
             '    });',
             '});'
-        ].join('\n')
+        ].join('\n'),
+        {
+            code: [
+                    'foo("describe1", function() {});',
+                    'foo("describe2", function() {});'
+                ].join('\n'),
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            }
+        }, {
+            code: [
+                    'foo("describe1", function() {});',
+                    'foo("describe2", function() {});'
+                ].join('\n'),
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           }
+         }
     ],
 
     invalid: [
@@ -154,7 +173,28 @@ ruleTester.run('no-identical-title', rules['no-identical-title'], {
                 'describe("describe1", function() {});'
             ].join('\n'),
             errors: [ { message: 'Test suite title is used multiple times.', column: 1, line: 4 } ]
-        }
+        },
+        {
+            code: [
+                    'foo("describe1", function() {});',
+                    'foo("describe1", function() {});'
+                ].join('\n'),
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            },
+            errors: [ { message: 'Test suite title is used multiple times.', column: 1, line: 2 } ]
+        }, {
+            code: [
+                    'foo("describe1", function() {});',
+                    'foo("describe1", function() {});'
+                ].join('\n'),
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           },
+           errors: [ { message: 'Test suite title is used multiple times.', column: 1, line: 2 } ]
+         }
     ]
 
 });
diff --git a/test/rules/no-nested-tests.js b/test/rules/no-nested-tests.js
index a3c126c..32e04b0 100644
--- a/test/rules/no-nested-tests.js
+++ b/test/rules/no-nested-tests.js
@@ -9,7 +9,20 @@ ruleTester.run('no-nested-tests', rule, {
         'it()',
         'it(); it(); it()',
         'describe("", function () { it(); })',
-        'describe("", function () { describe("", function () { it(); }); it(); })'
+        'describe("", function () { describe("", function () { it(); }); it(); })',
+        {
+            code: 'foo("", function () { it(); })',
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            }
+        }, {
+            code: 'foo("", function () { it(); })',
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           }
+        }
     ],
 
     invalid: [
@@ -104,6 +117,30 @@ ruleTester.run('no-nested-tests', rule, {
                     column: 55
                 }
             ]
+        },
+        {
+            code: 'it("", function () { foo() });',
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            },
+            errors: [ {
+                message: 'Unexpected suite nested within a test.',
+                line: 1,
+                column: 22
+            } ]
+        },
+        {
+            code: 'it("", function () { foo() });',
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+            },
+            errors: [ {
+                message: 'Unexpected suite nested within a test.',
+                line: 1,
+                column: 22
+            } ]
         }
     ]
 });
diff --git a/test/rules/no-sibling-hooks.js b/test/rules/no-sibling-hooks.js
index 1616458..ec88959 100644
--- a/test/rules/no-sibling-hooks.js
+++ b/test/rules/no-sibling-hooks.js
@@ -71,7 +71,34 @@ ruleTester.run('no-sibling-hooks', rules['no-sibling-hooks'], {
           '    });',
           '    before(function() {});',
           '});'
-        ].join('\n')
+        ].join('\n'),
+        {
+            code: [
+                'foo(function() {',
+                '    foo(function() {',
+                '        before(function() {});',
+                '    });',
+                '    before(function() {});',
+                '});'
+                ].join('\n'),
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            }
+        }, {
+            code: [
+                'foo(function() {',
+                '    foo(function() {',
+                '        before(function() {});',
+                '    });',
+                '    before(function() {});',
+                '});'
+                ].join('\n'),
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           }
+         }
     ],
 
     invalid: [
@@ -114,7 +141,38 @@ ruleTester.run('no-sibling-hooks', rules['no-sibling-hooks'], {
               '});'
             ].join('\n'),
             errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
-        }
+        },
+        {
+            code: [
+              'foo(function() {',
+              '    before(function() {});',
+              '    foo(function() {',
+              '        before(function() {});',
+              '    });',
+              '    before(function() {});',
+              '});'
+            ].join('\n'),
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            },
+            errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
+        }, {
+            code: [
+              'foo(function() {',
+              '    before(function() {});',
+              '    foo(function() {',
+              '        before(function() {});',
+              '    });',
+              '    before(function() {});',
+              '});'
+            ].join('\n'),
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           },
+           errors: [ { message: 'Unexpected use of duplicate Mocha `before` hook', column: 5, line: 6 } ]
+         }
     ]
 
 });
diff --git a/test/rules/no-top-level-hooks.js b/test/rules/no-top-level-hooks.js
index 59710b1..ec52ef7 100644
--- a/test/rules/no-top-level-hooks.js
+++ b/test/rules/no-top-level-hooks.js
@@ -21,9 +21,21 @@ ruleTester.run('no-top-level-hooks', rules['no-top-level-hooks'], {
         'after.foo()',
         'beforeEach.foo()',
         'afterEach.foo()',
-        'var before = 2; before + 3;'
+        'var before = 2; before + 3;',
+        {
+            code: 'foo(function() { before(function() {}); });',
+            settings: {
+                'mocha/additionalSuiteNames': [ 'foo' ]
+            }
+        }, {
+            code: 'foo(function() { before(function() {}); });',
+            settings: {
+                mocha: {
+                   additionalSuiteNames: [ 'foo' ]
+               }
+           }
+         }
     ],
-
     invalid: [
         {
             code: 'before(function() {});',
@@ -72,6 +84,14 @@ ruleTester.run('no-top-level-hooks', rules['no-top-level-hooks'], {
                 column: 1,
                 line: 1
             } ]
+        },
+        {
+            code: 'foo(function() {}); before(function() {});',
+            errors: [ {
+                message: 'Unexpected use of Mocha `before` hook outside of a test suite',
+                column: 21,
+                line: 1
+            } ]
         }
     ]