From f75824359f2242f53997c59c238d83a59badeea3 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sat, 3 Apr 2021 09:05:54 +1300 Subject: [PATCH] fix(prefer-expect-assertions): support `.each` (#798) Fixes #676 --- src/rules/__tests__/lowercase-name.test.ts | 1 + .../prefer-expect-assertions.test.ts | 181 ++++++++++++++++++ src/rules/lowercase-name.ts | 4 +- src/rules/prefer-expect-assertions.ts | 38 ++-- src/rules/utils.ts | 9 +- 5 files changed, 212 insertions(+), 21 deletions(-) diff --git a/src/rules/__tests__/lowercase-name.test.ts b/src/rules/__tests__/lowercase-name.test.ts index 6510d6b41..9c608ea0d 100644 --- a/src/rules/__tests__/lowercase-name.test.ts +++ b/src/rules/__tests__/lowercase-name.test.ts @@ -14,6 +14,7 @@ const ruleTester = new TSESLint.RuleTester({ ruleTester.run('lowercase-name', rule, { valid: [ 'it.each()', + 'it.each()(1)', 'randomFunction()', 'foo.bar()', 'it()', diff --git a/src/rules/__tests__/prefer-expect-assertions.test.ts b/src/rules/__tests__/prefer-expect-assertions.test.ts index d09ab5c12..6c66625c1 100644 --- a/src/rules/__tests__/prefer-expect-assertions.test.ts +++ b/src/rules/__tests__/prefer-expect-assertions.test.ts @@ -12,6 +12,7 @@ const ruleTester = new TSESLint.RuleTester({ ruleTester.run('prefer-expect-assertions', rule, { valid: [ + 'test("nonsense", [])', 'test("it1", () => {expect.assertions(0);})', 'test("it1", function() {expect.assertions(0);})', 'test("it1", function() {expect.hasAssertions();})', @@ -259,3 +260,183 @@ ruleTester.run('prefer-expect-assertions', rule, { }, ], }); + +ruleTester.run('.each support', rule, { + valid: [ + 'test.each()("is fine", () => { expect.assertions(0); })', + 'test.each``("is fine", () => { expect.assertions(0); })', + 'test.each()("is fine", () => { expect.hasAssertions(); })', + 'test.each``("is fine", () => { expect.hasAssertions(); })', + 'it.each()("is fine", () => { expect.assertions(0); })', + 'it.each``("is fine", () => { expect.assertions(0); })', + 'it.each()("is fine", () => { expect.hasAssertions(); })', + 'it.each``("is fine", () => { expect.hasAssertions(); })', + { + code: 'test.each()("is fine", () => {})', + options: [{ onlyFunctionsWithAsyncKeyword: true }], + }, + { + code: 'test.each``("is fine", () => {})', + options: [{ onlyFunctionsWithAsyncKeyword: true }], + }, + { + code: 'it.each()("is fine", () => {})', + options: [{ onlyFunctionsWithAsyncKeyword: true }], + }, + { + code: 'it.each``("is fine", () => {})', + options: [{ onlyFunctionsWithAsyncKeyword: true }], + }, + dedent` + describe.each(['hello'])('%s', () => { + it('is fine', () => { + expect.assertions(0); + }); + }); + `, + dedent` + describe.each\`\`('%s', () => { + it('is fine', () => { + expect.assertions(0); + }); + }); + `, + dedent` + describe.each(['hello'])('%s', () => { + it('is fine', () => { + expect.hasAssertions(); + }); + }); + `, + dedent` + describe.each\`\`('%s', () => { + it('is fine', () => { + expect.hasAssertions(); + }); + }); + `, + dedent` + describe.each(['hello'])('%s', () => { + it.each()('is fine', () => { + expect.assertions(0); + }); + }); + `, + dedent` + describe.each\`\`('%s', () => { + it.each()('is fine', () => { + expect.assertions(0); + }); + }); + `, + dedent` + describe.each(['hello'])('%s', () => { + it.each()('is fine', () => { + expect.hasAssertions(); + }); + }); + `, + dedent` + describe.each\`\`('%s', () => { + it.each()('is fine', () => { + expect.hasAssertions(); + }); + }); + `, + ], + invalid: [ + { + code: dedent` + test.each()("is not fine", () => { + expect(someValue).toBe(true); + }); + `, + errors: [ + { + messageId: 'haveExpectAssertions', + column: 1, + line: 1, + }, + ], + }, + { + code: dedent` + describe.each()('something', () => { + it("is not fine", () => { + expect(someValue).toBe(true); + }); + }); + `, + errors: [ + { + messageId: 'haveExpectAssertions', + column: 3, + line: 2, + }, + ], + }, + { + code: dedent` + describe.each()('something', () => { + test.each()("is not fine", () => { + expect(someValue).toBe(true); + }); + }); + `, + errors: [ + { + messageId: 'haveExpectAssertions', + column: 3, + line: 2, + }, + ], + }, + { + code: dedent` + test.each()("is not fine", async () => { + expect(someValue).toBe(true); + }); + `, + options: [{ onlyFunctionsWithAsyncKeyword: true }], + errors: [ + { + messageId: 'haveExpectAssertions', + column: 1, + line: 1, + }, + ], + }, + { + code: dedent` + it.each()("is not fine", async () => { + expect(someValue).toBe(true); + }); + `, + options: [{ onlyFunctionsWithAsyncKeyword: true }], + errors: [ + { + messageId: 'haveExpectAssertions', + column: 1, + line: 1, + }, + ], + }, + { + code: dedent` + describe.each()('something', () => { + test.each()("is not fine", async () => { + expect(someValue).toBe(true); + }); + }); + `, + options: [{ onlyFunctionsWithAsyncKeyword: true }], + errors: [ + { + messageId: 'haveExpectAssertions', + column: 3, + line: 2, + }, + ], + }, + ], +}); diff --git a/src/rules/lowercase-name.ts b/src/rules/lowercase-name.ts index 041fc2531..a083c29b4 100644 --- a/src/rules/lowercase-name.ts +++ b/src/rules/lowercase-name.ts @@ -35,8 +35,8 @@ const findNodeNameAndArgument = ( if (isEachCall(node)) { if ( - node.parent?.type === AST_NODE_TYPES.CallExpression && - hasStringAsFirstArgument(node.parent) + node.parent.arguments.length > 0 && + isStringNode(node.parent.arguments[0]) ) { return [node.callee.object.name, node.parent.arguments[0]]; } diff --git a/src/rules/prefer-expect-assertions.ts b/src/rules/prefer-expect-assertions.ts index c65ad3c6b..d62d76dfd 100644 --- a/src/rules/prefer-expect-assertions.ts +++ b/src/rules/prefer-expect-assertions.ts @@ -8,7 +8,10 @@ import { createRule, getAccessorValue, hasOnlyOneArgument, + isEachCall, + isFunction, isSupportedAccessor, + isTestCase, } from './utils'; const isExpectAssertionsOrHasAssertionsCall = ( @@ -40,12 +43,6 @@ const suggestRemovingExtraArguments = ( ]), }); -interface PreferExpectAssertionsCallExpression extends TSESTree.CallExpression { - arguments: [ - TSESTree.Expression, - TSESTree.ArrowFunctionExpression & { body: TSESTree.BlockStatement }, - ]; -} interface RuleOptions { onlyFunctionsWithAsyncKeyword?: boolean; } @@ -103,14 +100,28 @@ export default createRule<[RuleOptions], MessageIds>({ defaultOptions: [{ onlyFunctionsWithAsyncKeyword: false }], create(context, [options]) { return { - 'CallExpression[callee.name=/^(it|test)$/][arguments.1.body.body]'( - node: PreferExpectAssertionsCallExpression, - ) { - if (options.onlyFunctionsWithAsyncKeyword && !node.arguments[1].async) { + CallExpression(node: TSESTree.CallExpression) { + if (!isTestCase(node)) { + return; + } + + const args = isEachCall(node) ? node.parent.arguments : node.arguments; + + if (args.length < 2) { + return; + } + + const [, testFn] = args; + + if ( + !isFunction(testFn) || + testFn.body.type !== AST_NODE_TYPES.BlockStatement || + (options.onlyFunctionsWithAsyncKeyword && !testFn.async) + ) { return; } - const testFuncBody = node.arguments[1].body.body; + const testFuncBody = testFn.body.body; if (!isFirstLineExprStmt(testFuncBody)) { context.report({ @@ -120,10 +131,7 @@ export default createRule<[RuleOptions], MessageIds>({ messageId, fix: fixer => fixer.insertTextBeforeRange( - [ - node.arguments[1].body.range[0] + 1, - node.arguments[1].body.range[1], - ], + [testFn.body.range[0] + 1, testFn.body.range[1]], text, ), })), diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 77c4f0e93..1e701da7b 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -692,19 +692,20 @@ export const isDescribe = ( DescribeAlias.hasOwnProperty(node.callee.tag.object.name)); /** - * Checks if the given node` is a call to `.each(...)`. - * If `true`, the code must look like `.each(...)`. + * Checks if the given node` is a call to `.each(...)()`. + * If `true`, the code must look like `.each(...)()`. * * @param {JestFunctionCallExpression} node * - * @return {node is JestFunctionCallExpressionWithMemberExpressionCallee} + * @return {node is JestFunctionCallExpressionWithMemberExpressionCallee & {parent: TSESTree.CallExpression}} */ export const isEachCall = ( node: JestFunctionCallExpression, ): node is JestFunctionCallExpressionWithMemberExpressionCallee< DescribeAlias | TestCaseName, DescribeProperty.each | TestCaseProperty.each -> => +> & { parent: TSESTree.CallExpression } => + node.parent?.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.callee.property, DescribeProperty.each);