diff --git a/.eslintrc.json b/.eslintrc.json index a644354..ede75d7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,8 @@ "files": [ "test/**/*.js", "benchmarks/**/*.js" ], "env": { "mocha": true }, "rules": { - "max-nested-callbacks": [ "error", 8 ] + "max-nested-callbacks": [ "error", 8 ], + "max-statements": [ "error", 30 ] } } ] diff --git a/docs/rules/handle-done-callback.md b/docs/rules/handle-done-callback.md index 07dd174..5306a6c 100644 --- a/docs/rules/handle-done-callback.md +++ b/docs/rules/handle-done-callback.md @@ -59,6 +59,19 @@ before(function (done) { }); }); ``` +## Options + +This rule supports the following options: + +* `ignoreSkipped`: When set to `true` skipped test cases won’t be checked. Defaults to `false`. + +```json +{ + "rules": { + "mocha/handle-done-callback": ["error", {"ignoreSkipped": true}] + } +} +``` ## When Not To Use It diff --git a/lib/rules/handle-done-callback.js b/lib/rules/handle-done-callback.js index 7f8ed99..49b6d3e 100644 --- a/lib/rules/handle-done-callback.js +++ b/lib/rules/handle-done-callback.js @@ -8,9 +8,25 @@ module.exports = { type: 'problem', docs: { description: 'Enforces handling of callbacks for async tests' - } + }, + schema: [ + { + type: 'object', + properties: { + ignoreSkipped: { + type: 'boolean', + default: false + } + }, + additionalProperties: false + } + ] }, create(context) { + const [ config = {} ] = context.options; + const { ignoreSkipped = false } = config; + const modifiersToCheck = ignoreSkipped ? [ 'only' ] : [ 'only', 'skip' ]; + function isAsyncFunction(functionExpression) { return functionExpression.params.length === 1; } @@ -43,7 +59,7 @@ module.exports = { } function check(node) { - if (astUtils.hasParentMochaFunctionCall(node) && isAsyncFunction(node)) { + if (astUtils.hasParentMochaFunctionCall(node, { modifiers: modifiersToCheck }) && isAsyncFunction(node)) { checkAsyncMochaFunction(node); } } diff --git a/lib/util/ast.js b/lib/util/ast.js index 85db2c1..b36f3ad 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -6,25 +6,16 @@ const isNil = require('ramda/src/isNil'); const propEq = require('ramda/src/propEq'); const pathEq = require('ramda/src/pathEq'); const find = require('ramda/src/find'); +const { getTestCaseNames, getSuiteNames } = require('./names'); const isDefined = complement(isNil); const isCallExpression = both(isDefined, propEq('type', 'CallExpression')); -const describeAliases = [ - 'describe', 'xdescribe', 'describe.only', 'describe.skip', - 'context', 'xcontext', 'context.only', 'context.skip', - 'suite', 'xsuite', 'suite.only', 'suite.skip' -]; const hooks = [ 'before', 'after', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll', 'setup', 'teardown', 'suiteSetup', 'suiteTeardown' ]; const suiteConfig = [ 'timeout', 'slow', 'retries' ]; -const testCaseNames = [ - 'it', 'it.only', 'it.skip', 'xit', - 'test', 'test.only', 'test.skip', - 'specify', 'specify.only', 'specify.skip', 'xspecify' -]; function getPropertyName(property) { return property.name || property.value; @@ -38,8 +29,9 @@ function getNodeName(node) { } function isDescribe(node, additionalSuiteNames = []) { - return isCallExpression(node) && - describeAliases.concat(additionalSuiteNames).indexOf(getNodeName(node.callee)) > -1; + const describeAliases = getSuiteNames({ modifiers: [ 'skip', 'only' ], additionalSuiteNames }); + + return isCallExpression(node) && describeAliases.indexOf(getNodeName(node.callee)) > -1; } function isHookIdentifier(node) { @@ -52,7 +44,10 @@ function isHookCall(node) { return isCallExpression(node) && isHookIdentifier(node.callee); } -function isTestCase(node) { +function isTestCase(node, options = {}) { + const { modifiers = [ 'skip', 'only' ] } = options; + + const testCaseNames = getTestCaseNames({ modifiers }); return isCallExpression(node) && testCaseNames.indexOf(getNodeName(node.callee)) > -1; } @@ -100,8 +95,8 @@ function isMochaFunctionCall(node, scope) { return isTestCase(node) || isDescribe(node) || isHookCall(node); } -function hasParentMochaFunctionCall(functionExpression) { - return isTestCase(functionExpression.parent) || isHookCall(functionExpression.parent); +function hasParentMochaFunctionCall(functionExpression, options) { + return isTestCase(functionExpression.parent, options) || isHookCall(functionExpression.parent); } function isExplicitUndefined(node) { diff --git a/lib/util/names.js b/lib/util/names.js new file mode 100644 index 0000000..1b41f27 --- /dev/null +++ b/lib/util/names.js @@ -0,0 +1,77 @@ +'use strict'; + +const chain = require('ramda/src/chain'); + +const suiteNames = [ + 'describe', + 'context', + 'suite' +]; + +const suiteModifiers = { + skip: [ + 'describe.skip', + 'context.skip', + 'suite.skip', + 'xdescribe', + 'xcontext', + 'xsuite' + ], + only: [ + 'describe.only', + 'context.only', + 'suite.only' + ] +}; + +const testCaseNames = [ + 'it', + 'test', + 'specify' +]; + +const testCaseModifiers = { + skip: [ + 'it.skip', + 'test.skip', + 'specify.skip', + 'xit', + 'xspecify' + ], + only: [ + 'it.only', + 'test.only', + 'specify.only' + ] +}; + +function getTestCaseNames(options = {}) { + const { modifiers = [], baseNames = true } = options; + const names = baseNames ? testCaseNames : []; + + return names.concat(chain((modifierName) => { + if (testCaseModifiers[modifierName]) { + return testCaseModifiers[modifierName]; + } + + return []; + }, modifiers)); +} + +function getSuiteNames(options = {}) { + const { modifiers = [], baseNames = true, additionalSuiteNames = [] } = options; + const names = baseNames ? suiteNames.concat(additionalSuiteNames) : []; + + return names.concat(chain((modifierName) => { + if (suiteModifiers[modifierName]) { + return suiteModifiers[modifierName]; + } + + return []; + }, modifiers)); +} + +module.exports = { + getTestCaseNames, + getSuiteNames +}; diff --git a/test/rules/handle-done-callback.js b/test/rules/handle-done-callback.js index fcee12f..6f235ad 100644 --- a/test/rules/handle-done-callback.js +++ b/test/rules/handle-done-callback.js @@ -31,6 +31,10 @@ ruleTester.run('handle-done-callback', rules['handle-done-callback'], { { code: 'it("", (done) => { done(); });', parserOptions: { ecmaVersion: 6 } + }, + { + code: 'it.skip("", function (done) { });', + options: [ { ignoreSkipped: true } ] } ], @@ -39,6 +43,14 @@ ruleTester.run('handle-done-callback', rules['handle-done-callback'], { code: 'it("", function (done) { });', errors: [ { message: 'Expected "done" callback to be handled.', column: 18, line: 1 } ] }, + { + code: 'it.skip("", function (done) { });', + errors: [ { message: 'Expected "done" callback to be handled.', column: 23, line: 1 } ] + }, + { + code: 'xit("", function (done) { });', + errors: [ { message: 'Expected "done" callback to be handled.', column: 19, line: 1 } ] + }, { code: 'it("", function (done) { callback(); });', errors: [ { message: 'Expected "done" callback to be handled.', column: 18, line: 1 } ] diff --git a/test/util/namesSpec.js b/test/util/namesSpec.js new file mode 100644 index 0000000..545c01c --- /dev/null +++ b/test/util/namesSpec.js @@ -0,0 +1,257 @@ +'use strict'; + +const { expect } = require('chai'); +const { getTestCaseNames, getSuiteNames } = require('../../lib/util/names'); + +describe('mocha names', () => { + describe('test case names', () => { + it('returns the list of basic test case names when no options are provided', () => { + const testCaseNames = getTestCaseNames(); + + expect(testCaseNames).to.deep.equal([ + 'it', + 'test', + 'specify' + ]); + }); + + it('returns an empty list when no modifiers and no base names are wanted', () => { + const testCaseNames = getTestCaseNames({ baseNames: false }); + + expect(testCaseNames).to.deep.equal([]); + }); + + it('always returns a new array', () => { + const testCaseNames1 = getTestCaseNames({ baseNames: false }); + const testCaseNames2 = getTestCaseNames({ baseNames: false }); + + expect(testCaseNames1).to.deep.equal(testCaseNames2); + expect(testCaseNames1).to.not.equal(testCaseNames2); + }); + + it('ignores invalid modifiers', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'foo' ], baseNames: false }); + + expect(testCaseNames).to.deep.equal([]); + }); + + it('returns the list of test case names with and without "skip" modifiers applied', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'skip' ] }); + + expect(testCaseNames).to.deep.equal([ + 'it', + 'test', + 'specify', + 'it.skip', + 'test.skip', + 'specify.skip', + 'xit', + 'xspecify' + ]); + }); + + it('returns the list of test case names only with "skip" modifiers applied', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'skip' ], baseNames: false }); + + expect(testCaseNames).to.deep.equal([ + 'it.skip', + 'test.skip', + 'specify.skip', + 'xit', + 'xspecify' + ]); + }); + + it('returns the list of test case names with and without "only" modifiers applied', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'only' ] }); + + expect(testCaseNames).to.deep.equal([ + 'it', + 'test', + 'specify', + 'it.only', + 'test.only', + 'specify.only' + ]); + }); + + it('returns the list of test case names only with "only" modifiers applied', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'only' ], baseNames: false }); + + expect(testCaseNames).to.deep.equal([ + 'it.only', + 'test.only', + 'specify.only' + ]); + }); + + it('returns the list of all test case names', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'skip', 'only' ] }); + + expect(testCaseNames).to.deep.equal([ + 'it', + 'test', + 'specify', + 'it.skip', + 'test.skip', + 'specify.skip', + 'xit', + 'xspecify', + 'it.only', + 'test.only', + 'specify.only' + ]); + }); + + it('returns the list of test case names only with modifiers applied', () => { + const testCaseNames = getTestCaseNames({ modifiers: [ 'skip', 'only' ], baseNames: false }); + + expect(testCaseNames).to.deep.equal([ + 'it.skip', + 'test.skip', + 'specify.skip', + 'xit', + 'xspecify', + 'it.only', + 'test.only', + 'specify.only' + ]); + }); + }); + + describe('suite names', () => { + it('returns the list of basic suite names when no options are provided', () => { + const suiteNames = getSuiteNames(); + + expect(suiteNames).to.deep.equal([ + 'describe', + 'context', + 'suite' + ]); + }); + + it('returns an empty list when no modifiers and no base names are wanted', () => { + const suiteNames = getSuiteNames({ baseNames: false }); + + expect(suiteNames).to.deep.equal([]); + }); + + it('always returns a new array', () => { + const suiteNames1 = getSuiteNames({ baseNames: false }); + const suiteNames2 = getSuiteNames({ baseNames: false }); + + expect(suiteNames1).to.deep.equal(suiteNames2); + expect(suiteNames1).to.not.equal(suiteNames2); + }); + + it('ignores invalid modifiers', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'foo' ], baseNames: false }); + + expect(suiteNames).to.deep.equal([]); + }); + + it('returns the list of suite names with and without "skip" modifiers applied', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'skip' ] }); + + expect(suiteNames).to.deep.equal([ + 'describe', + 'context', + 'suite', + 'describe.skip', + 'context.skip', + 'suite.skip', + 'xdescribe', + 'xcontext', + 'xsuite' + ]); + }); + + it('returns the list of suite names only with "skip" modifiers applied', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'skip' ], baseNames: false }); + + expect(suiteNames).to.deep.equal([ + 'describe.skip', + 'context.skip', + 'suite.skip', + 'xdescribe', + 'xcontext', + 'xsuite' + ]); + }); + + it('returns the list of suite names with and without "only" modifiers applied', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'only' ] }); + + expect(suiteNames).to.deep.equal([ + 'describe', + 'context', + 'suite', + 'describe.only', + 'context.only', + 'suite.only' + ]); + }); + + it('returns the list of suite names only with "only" modifiers applied', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'only' ], baseNames: false }); + + expect(suiteNames).to.deep.equal([ + 'describe.only', + 'context.only', + 'suite.only' + ]); + }); + + it('returns the list of all suite names', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'skip', 'only' ] }); + + expect(suiteNames).to.deep.equal([ + 'describe', + 'context', + 'suite', + 'describe.skip', + 'context.skip', + 'suite.skip', + 'xdescribe', + 'xcontext', + 'xsuite', + 'describe.only', + 'context.only', + 'suite.only' + ]); + }); + + it('returns the list of suite names names only with modifiers applied', () => { + const suiteNames = getSuiteNames({ modifiers: [ 'skip', 'only' ], baseNames: false }); + + expect(suiteNames).to.deep.equal([ + 'describe.skip', + 'context.skip', + 'suite.skip', + 'xdescribe', + 'xcontext', + 'xsuite', + 'describe.only', + 'context.only', + 'suite.only' + ]); + }); + + it('returns the additional suite names', () => { + const suiteNames = getSuiteNames({ additionalSuiteNames: [ 'myCustomDescribe' ] }); + + expect(suiteNames).to.deep.equal([ + 'describe', + 'context', + 'suite', + 'myCustomDescribe' + ]); + }); + + it('doesn’t return the additional suite names when base names shouldn’t be included', () => { + const suiteNames = getSuiteNames({ additionalSuiteNames: [ 'myCustomDescribe' ], baseNames: false }); + + expect(suiteNames).to.deep.equal([]); + }); + }); +});