diff --git a/index.js b/index.js index 1e1ea87e2..f8802796c 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const preferToBeNull = require('./rules/prefer_to_be_null'); const preferToBeUndefined = require('./rules/prefer_to_be_undefined'); const preferToHaveLength = require('./rules/prefer_to_have_length'); const validExpect = require('./rules/valid_expect'); +const preferExpectAssertions = require('./rules/prefer_expect_assertions'); const snapshotProcessor = require('./processors/snapshot-processor'); @@ -62,5 +63,6 @@ module.exports = { 'prefer-to-be-undefined': preferToBeUndefined, 'prefer-to-have-length': preferToHaveLength, 'valid-expect': validExpect, + 'prefer-expect-assertions': preferExpectAssertions, }, }; diff --git a/rules/__tests__/prefer_expect_assertions.test.js b/rules/__tests__/prefer_expect_assertions.test.js new file mode 100644 index 000000000..417b7cb34 --- /dev/null +++ b/rules/__tests__/prefer_expect_assertions.test.js @@ -0,0 +1,91 @@ +'use strict'; + +const RuleTester = require('eslint').RuleTester; +const rules = require('../..').rules; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + }, +}); + +const expectedMsg = + 'Every test should have either `expect.assertions()` or `expect.hasAssertions()` as its first expression'; + +ruleTester.run('prefer-expect-assertions', rules['prefer-expect-assertions'], { + invalid: [ + { + code: 'it("it1", () => {})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + { + code: 'it("it1", () => { foo()})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + { + code: + 'it("it1", function() {' + + '\n\t\t\tsomeFunctionToDo();' + + '\n\t\t\tsomeFunctionToDo2();\n' + + '\t\t\t})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + { + code: 'it("it1", function() {var a = 2;})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + { + code: 'it("it1", function() {expect.assertions();})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + { + code: 'it("it1", function() {expect.assertions(1,2);})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + { + code: 'it("it1", function() {expect.assertions("1");})', + errors: [ + { + message: expectedMsg, + }, + ], + }, + ], + + valid: [ + { + code: 'test("it1", () => {expect.assertions(0);})', + }, + 'test("it1", function() {expect.assertions(0);})', + 'test("it1", function() {expect.hasAssertions();})', + 'it("it1", function() {expect.assertions(0);})', + 'it("it1", function() {\n\t\t\texpect.assertions(1);' + + '\n\t\t\texpect(someValue).toBe(true)\n' + + '\t\t\t})', + 'test("it1")', + ], +}); diff --git a/rules/prefer_expect_assertions.js b/rules/prefer_expect_assertions.js new file mode 100644 index 000000000..d8f128f8e --- /dev/null +++ b/rules/prefer_expect_assertions.js @@ -0,0 +1,81 @@ +'use strict'; + +const ruleMsg = + 'Every test should have either `expect.assertions()` or `expect.hasAssertions()` as its first expression'; + +const validateArguments = expression => { + return ( + expression.arguments && + expression.arguments.length === 1 && + Number.isInteger(expression.arguments[0].value) + ); +}; + +const isExpectAssertionsOrHasAssertionsCall = expression => { + try { + const expectAssertionOrHasAssertionCall = + expression.type === 'CallExpression' && + expression.callee.type === 'MemberExpression' && + expression.callee.object.name === 'expect' && + (expression.callee.property.name === 'assertions' || + expression.callee.property.name === 'hasAssertions'); + + if (expression.callee.property.name === 'assertions') { + return expectAssertionOrHasAssertionCall && validateArguments(expression); + } + return expectAssertionOrHasAssertionCall; + } catch (e) { + return false; + } +}; + +const isTestOrItFunction = node => { + return ( + node.type === 'CallExpression' && + node.callee && + (node.callee.name === 'it' || node.callee.name === 'test') + ); +}; + +const getFunctionFirstLine = functionBody => { + return functionBody[0] && functionBody[0].expression; +}; + +const isFirstLineExprStmt = functionBody => { + return functionBody[0] && functionBody[0].type === 'ExpressionStatement'; +}; + +const getTestFunctionBody = node => { + try { + return node.arguments[1].body.body; + } catch (e) { + return undefined; + } +}; + +const reportMsg = (context, node) => { + context.report({ + message: ruleMsg, + node, + }); +}; + +module.exports = context => { + return { + CallExpression(node) { + if (isTestOrItFunction(node)) { + const testFuncBody = getTestFunctionBody(node); + if (testFuncBody) { + if (!isFirstLineExprStmt(testFuncBody)) { + reportMsg(context, node); + } else { + const testFuncFirstLine = getFunctionFirstLine(testFuncBody); + if (!isExpectAssertionsOrHasAssertionsCall(testFuncFirstLine)) { + reportMsg(context, node); + } + } + } + } + }, + }; +};