From 9911ff89e24c05eb4401c979989ea1edcc17641c Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 14 Mar 2021 14:20:44 +1300 Subject: [PATCH] refactor(utils): support matching all variations of `describe` & `test` --- src/rules/__tests__/lowercase-name.test.ts | 4 + src/rules/__tests__/utils.test.ts | 424 +++++++++++++++++++++ src/rules/consistent-test-it.ts | 12 +- src/rules/lowercase-name.ts | 10 +- src/rules/no-conditional-expect.ts | 6 +- src/rules/no-done-callback.ts | 4 +- src/rules/no-duplicate-hooks.ts | 6 +- src/rules/no-export.ts | 4 +- src/rules/no-identical-title.ts | 12 +- src/rules/no-if.ts | 4 +- src/rules/no-standalone-expect.ts | 8 +- src/rules/no-test-prefixes.ts | 10 +- src/rules/no-test-return-statement.ts | 4 +- src/rules/no-try-expect.ts | 6 +- src/rules/prefer-hooks-on-top.ts | 4 +- src/rules/prefer-todo.ts | 4 +- src/rules/require-top-level-describe.ts | 10 +- src/rules/utils.ts | 132 ++++++- src/rules/valid-title.ts | 10 +- 19 files changed, 601 insertions(+), 73 deletions(-) create mode 100644 src/rules/__tests__/utils.test.ts diff --git a/src/rules/__tests__/lowercase-name.test.ts b/src/rules/__tests__/lowercase-name.test.ts index 6510d6b41..8280bb07d 100644 --- a/src/rules/__tests__/lowercase-name.test.ts +++ b/src/rules/__tests__/lowercase-name.test.ts @@ -51,6 +51,10 @@ ruleTester.run('lowercase-name', rule, { 'describe(function () {})', 'describe(``)', 'describe("")', + dedent` + describe.each()(1); + describe.each()(2); + `, 'describe(42)', { code: 'describe(42)', diff --git a/src/rules/__tests__/utils.test.ts b/src/rules/__tests__/utils.test.ts new file mode 100644 index 000000000..2f4ef69dc --- /dev/null +++ b/src/rules/__tests__/utils.test.ts @@ -0,0 +1,424 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import resolveFrom from 'resolve-from'; +import { createRule, isDescribeCall, isTestCaseCall } from '../utils'; + +const ruleTester = new TSESLint.RuleTester({ + parser: resolveFrom(require.resolve('eslint'), 'espree'), + parserOptions: { + ecmaVersion: 2015, + }, +}); + +const rule = createRule({ + name: __filename, + meta: { + docs: { + category: 'Possible Errors', + description: 'Fake rule for testing AST guards', + recommended: false, + }, + messages: { + foundDescribeCall: 'found a call to `describe`', + foundTestCaseCall: 'found a call to a test case', + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create: context => ({ + CallExpression(node) { + if (isDescribeCall(node)) { + context.report({ messageId: 'foundDescribeCall', node }); + } + + if (isTestCaseCall(node)) { + context.report({ messageId: 'foundTestCaseCall', node }); + } + }, + }), +}); + +ruleTester.run('nonexistent methods', rule, { + valid: [ + 'describe.something()', + 'describe.me()', + 'test.me()', + 'it.fails()', + 'context()', + 'context.each``()', + 'context.each()', + 'describe.context()', + 'describe.concurrent()()', + 'describe.concurrent``()', + 'describe.every``()', + ], + invalid: [], +}); + +ruleTester.run('it', rule, { + valid: [ + 'it["concurrent"]["skip"]', + 'it["concurrent"].skip', + 'it.concurrent["skip"]', + 'it.concurrent.skip', + + 'it["concurrent"]["only"]', + 'it["concurrent"].only', + 'it.concurrent["only"]', + 'it.concurrent.only', + + 'it["skip"]["each"]()', + 'it["skip"].each()', + 'it.skip["each"]()', + 'it.skip.each()', + + 'it["skip"]["each"]``', + 'it["skip"].each``', + 'it.skip["each"]``', + 'it.skip.each``', + + 'it["only"]["each"]()', + 'it["only"].each()', + 'it.only["each"]()', + 'it.only.each()', + + 'it["only"]["each"]``', + 'it["only"].each``', + 'it.only["each"]``', + 'it.only.each``', + + 'xit["each"]()', + 'xit.each()', + + 'xit["each"]``', + 'xit.each``', + + 'fit["each"]()', + 'fit.each()', + + 'fit["each"]``', + 'fit.each``', + + 'it["skip"]', + 'it.skip', + + 'it["only"]', + 'it.only', + + 'it["each"]()', + 'it.each()', + + 'it["each"]``', + 'it.each``', + + 'fit', + 'xit', + 'it', + ], + invalid: [ + ...[ + 'it["concurrent"]["skip"]()', + 'it["concurrent"].skip()', + 'it.concurrent["skip"]()', + 'it.concurrent.skip()', + + 'it["concurrent"]["only"]()', + 'it["concurrent"].only()', + 'it.concurrent["only"]()', + 'it.concurrent.only()', + + 'it["skip"]["each"]()()', + 'it["skip"].each()()', + 'it.skip["each"]()()', + 'it.skip.each()()', + + 'it["skip"]["each"]``()', + 'it["skip"].each``()', + 'it.skip["each"]``()', + 'it.skip.each``()', + + 'it["only"]["each"]()()', + 'it["only"].each()()', + 'it.only["each"]()()', + 'it.only.each()()', + + 'it["only"]["each"]``()', + 'it["only"].each``()', + + 'it.only["each"]``()', + 'it.only.each``()', + + 'xit["each"]()()', + 'xit.each()()', + + 'xit["each"]``()', + 'xit.each``()', + + 'fit["each"]()()', + 'fit.each()()', + + 'fit["each"]``()', + 'fit.each``()', + + 'it["skip"]()', + 'it.skip()', + + 'it["only"]()', + 'it.only()', + + 'it["each"]()()', + 'it.each()()', + + 'it["each"]``()', + 'it.each``()', + + 'fit()', + 'xit()', + 'it()', + ].map(code => ({ + code, + errors: [ + { + messageId: 'foundTestCaseCall' as const, + data: {}, + column: 1, + line: 1, + }, + ], + })), + ], +}); + +ruleTester.run('test', rule, { + valid: [ + 'test["concurrent"]["skip"]', + 'test["concurrent"].skip', + 'test.concurrent["skip"]', + 'test.concurrent.skip', + + 'test["concurrent"]["only"]', + 'test["concurrent"].only', + 'test.concurrent["only"]', + 'test.concurrent.only', + + 'test["skip"]["each"]()', + 'test["skip"].each()', + 'test.skip["each"]()', + 'test.skip.each()', + + 'test["skip"]["each"]``', + 'test["skip"].each``', + 'test.skip["each"]``', + 'test.skip.each``', + + 'test["only"]["each"]()', + 'test["only"].each()', + 'test.only["each"]()', + 'test.only.each()', + + 'test["only"]["each"]``', + 'test["only"].each``', + 'test.only["each"]``', + 'test.only.each``', + + 'xtest["each"]()', + 'xtest.each()', + + 'xtest["each"]``', + 'xtest.each``', + + 'test["skip"]', + 'test.skip', + + 'test["only"]', + 'test.only', + + 'test["each"]()', + 'test.each()', + + 'test["each"]``', + 'test.each``', + + 'xtest', + 'test', + ], + invalid: [ + ...[ + 'test["concurrent"]["skip"]()', + 'test["concurrent"].skip()', + 'test.concurrent["skip"]()', + 'test.concurrent.skip()', + + 'test["concurrent"]["only"]()', + 'test["concurrent"].only()', + 'test.concurrent["only"]()', + 'test.concurrent.only()', + + 'test["skip"]["each"]()()', + 'test["skip"].each()()', + 'test.skip["each"]()()', + 'test.skip.each()()', + + 'test["skip"]["each"]``()', + 'test["skip"].each``()', + 'test.skip["each"]``()', + 'test.skip.each``()', + + 'test["only"]["each"]()()', + 'test["only"].each()()', + 'test.only["each"]()()', + 'test.only.each()()', + + 'test["only"]["each"]``()', + 'test["only"].each``()', + + 'test.only["each"]``()', + 'test.only.each``()', + + 'xtest["each"]()()', + 'xtest.each()()', + + 'xtest["each"]``()', + 'xtest.each``()', + + 'test["skip"]()', + 'test.skip()', + + 'test["only"]()', + 'test.only()', + + 'test["each"]()()', + 'test.each()()', + + 'test["each"]``()', + 'test.each``()', + + 'xtest()', + 'test()', + ].map(code => ({ + code, + errors: [ + { + messageId: 'foundTestCaseCall' as const, + data: {}, + column: 1, + line: 1, + }, + ], + })), + ], +}); + +ruleTester.run('describe', rule, { + valid: [ + 'describe["skip"]["each"]()', + 'describe["skip"].each()', + 'describe.skip["each"]()', + 'describe.skip.each()', + + 'describe["skip"]["each"]``', + 'describe["skip"].each``', + 'describe.skip["each"]``', + 'describe.skip.each``', + + 'describe["only"]["each"]()', + 'describe["only"].each()', + 'describe.only["each"]()', + 'describe.only.each()', + + 'describe["only"]["each"]``', + 'describe["only"].each``', + 'describe.only["each"]``', + 'describe.only.each``', + + 'xdescribe["each"]()', + 'xdescribe.each()', + + 'xdescribe["each"]``', + 'xdescribe.each``', + + 'fdescribe["each"]()', + 'fdescribe.each()', + + 'fdescribe["each"]``', + 'fdescribe.each``', + + 'describe["skip"]', + 'describe.skip', + + 'describe["only"]', + 'describe.only', + + 'describe["each"]()', + 'describe.each()', + + 'describe["each"]``', + 'describe.each``', + + 'fdescribe', + 'xdescribe', + 'describe', + ], + invalid: [ + ...[ + 'describe["skip"]["each"]()()', + 'describe["skip"].each()()', + 'describe.skip["each"]()()', + 'describe.skip.each()()', + + 'describe["skip"]["each"]``()', + 'describe["skip"].each``()', + 'describe.skip["each"]``()', + 'describe.skip.each``()', + + 'describe["only"]["each"]()()', + 'describe["only"].each()()', + 'describe.only["each"]()()', + 'describe.only.each()()', + + 'describe["only"]["each"]``()', + 'describe["only"].each``()', + + 'describe.only["each"]``()', + 'describe.only.each``()', + + 'xdescribe["each"]()()', + 'xdescribe.each()()', + + 'xdescribe["each"]``()', + 'xdescribe.each``()', + + 'fdescribe["each"]()()', + 'fdescribe.each()()', + + 'fdescribe["each"]``()', + 'fdescribe.each``()', + + 'describe["skip"]()', + 'describe.skip()', + + 'describe["only"]()', + 'describe.only()', + + 'describe["each"]()()', + 'describe.each()()', + + 'describe["each"]``()', + 'describe.each``()', + + 'fdescribe()', + 'xdescribe()', + 'describe()', + ].map(code => ({ + code, + errors: [ + { + messageId: 'foundDescribeCall' as const, + data: {}, + column: 1, + line: 1, + }, + ], + })), + ], +}); diff --git a/src/rules/consistent-test-it.ts b/src/rules/consistent-test-it.ts index 75cc9025e..b58e0ee05 100644 --- a/src/rules/consistent-test-it.ts +++ b/src/rules/consistent-test-it.ts @@ -7,8 +7,8 @@ import { TestCaseName, createRule, getNodeName, - isDescribe, - isTestCase, + isDescribeCall, + isTestCaseCall, } from './utils'; const buildFixer = ( @@ -78,7 +78,7 @@ export default createRule< return; } - if (isDescribe(node)) { + if (isDescribeCall(node)) { describeNestingLevel++; } @@ -88,7 +88,7 @@ export default createRule< : node.callee; if ( - isTestCase(node) && + isTestCaseCall(node) && describeNestingLevel === 0 && !nodeName.includes(testKeyword) ) { @@ -103,7 +103,7 @@ export default createRule< } if ( - isTestCase(node) && + isTestCaseCall(node) && describeNestingLevel > 0 && !nodeName.includes(testKeywordWithinDescribe) ) { @@ -120,7 +120,7 @@ export default createRule< } }, 'CallExpression:exit'(node) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { describeNestingLevel--; } }, diff --git a/src/rules/lowercase-name.ts b/src/rules/lowercase-name.ts index 041fc2531..c9643b412 100644 --- a/src/rules/lowercase-name.ts +++ b/src/rules/lowercase-name.ts @@ -10,10 +10,10 @@ import { TestCaseName, createRule, getStringValue, - isDescribe, + isDescribeCall, isEachCall, isStringNode, - isTestCase, + isTestCaseCall, } from './utils'; type IgnorableFunctionExpressions = @@ -29,7 +29,7 @@ const hasStringAsFirstArgument = ( const findNodeNameAndArgument = ( node: TSESTree.CallExpression, ): [name: string, firstArg: StringNode] | null => { - if (!(isTestCase(node) || isDescribe(node))) { + if (!(isTestCaseCall(node) || isDescribeCall(node))) { return null; } @@ -116,7 +116,7 @@ export default createRule< return { CallExpression(node: TSESTree.CallExpression) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { numberOfDescribeBlocks++; if (ignoreTopLevelDescribe && numberOfDescribeBlocks === 1) { @@ -170,7 +170,7 @@ export default createRule< }); }, 'CallExpression:exit'(node: TSESTree.CallExpression) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { numberOfDescribeBlocks--; } }, diff --git a/src/rules/no-conditional-expect.ts b/src/rules/no-conditional-expect.ts index 2b9d91774..189ea0812 100644 --- a/src/rules/no-conditional-expect.ts +++ b/src/rules/no-conditional-expect.ts @@ -3,7 +3,7 @@ import { createRule, getTestCallExpressionsFromDeclaredVariables, isExpectCall, - isTestCase, + isTestCaseCall, } from './utils'; export default createRule({ @@ -40,7 +40,7 @@ export default createRule({ } }, CallExpression(node: TSESTree.CallExpression) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { inTestCase = true; } @@ -52,7 +52,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { inTestCase = false; } }, diff --git a/src/rules/no-done-callback.ts b/src/rules/no-done-callback.ts index 3e1ebecfc..cae56123c 100644 --- a/src/rules/no-done-callback.ts +++ b/src/rules/no-done-callback.ts @@ -7,7 +7,7 @@ import { getNodeName, isFunction, isHook, - isTestCase, + isTestCaseCall, } from './utils'; const findCallbackArg = ( @@ -22,7 +22,7 @@ const findCallbackArg = ( return node.arguments[0]; } - if (isTestCase(node) && node.arguments.length >= 2) { + if (isTestCaseCall(node) && node.arguments.length >= 2) { return node.arguments[1]; } diff --git a/src/rules/no-duplicate-hooks.ts b/src/rules/no-duplicate-hooks.ts index d06da84b6..e8b02754a 100644 --- a/src/rules/no-duplicate-hooks.ts +++ b/src/rules/no-duplicate-hooks.ts @@ -1,4 +1,4 @@ -import { createRule, isDescribe, isHook } from './utils'; +import { createRule, isDescribeCall, isHook } from './utils'; const newHookContext = () => ({ beforeAll: 0, @@ -27,7 +27,7 @@ export default createRule({ return { CallExpression(node) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { hookContexts.push(newHookContext()); } @@ -45,7 +45,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { hookContexts.pop(); } }, diff --git a/src/rules/no-export.ts b/src/rules/no-export.ts index 7547c4bc3..e3c92d9cc 100644 --- a/src/rules/no-export.ts +++ b/src/rules/no-export.ts @@ -2,7 +2,7 @@ import { AST_NODE_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; -import { createRule, isTestCase } from './utils'; +import { createRule, isTestCaseCall } from './utils'; export default createRule({ name: __filename, @@ -37,7 +37,7 @@ export default createRule({ }, CallExpression(node) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { hasTestCase = true; } }, diff --git a/src/rules/no-identical-title.ts b/src/rules/no-identical-title.ts index 1c5f59efc..f83afb5f2 100644 --- a/src/rules/no-identical-title.ts +++ b/src/rules/no-identical-title.ts @@ -2,9 +2,9 @@ import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import { createRule, getStringValue, - isDescribe, + isDescribeCall, isStringNode, - isTestCase, + isTestCaseCall, } from './utils'; interface DescribeContext { @@ -42,7 +42,7 @@ export default createRule({ CallExpression(node) { const currentLayer = contexts[contexts.length - 1]; - if (isDescribe(node)) { + if (isDescribeCall(node)) { contexts.push(newDescribeContext()); } @@ -58,7 +58,7 @@ export default createRule({ const title = getStringValue(argument); - if (isTestCase(node)) { + if (isTestCaseCall(node)) { if (currentLayer.testTitles.includes(title)) { context.report({ messageId: 'multipleTestTitle', @@ -68,7 +68,7 @@ export default createRule({ currentLayer.testTitles.push(title); } - if (!isDescribe(node)) { + if (!isDescribeCall(node)) { return; } if (currentLayer.describeTitles.includes(title)) { @@ -80,7 +80,7 @@ export default createRule({ currentLayer.describeTitles.push(title); }, 'CallExpression:exit'(node) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { contexts.pop(); } }, diff --git a/src/rules/no-if.ts b/src/rules/no-if.ts index 664a27955..f8ba2f236 100644 --- a/src/rules/no-if.ts +++ b/src/rules/no-if.ts @@ -7,7 +7,7 @@ import { createRule, getNodeName, getTestCallExpressionsFromDeclaredVariables, - isTestCase, + isTestCaseCall, } from './utils'; const testCaseNames = new Set([ @@ -75,7 +75,7 @@ export default createRule({ return { CallExpression(node) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { stack.push(true); } }, diff --git a/src/rules/no-standalone-expect.ts b/src/rules/no-standalone-expect.ts index 4daa3c847..247775673 100644 --- a/src/rules/no-standalone-expect.ts +++ b/src/rules/no-standalone-expect.ts @@ -7,10 +7,10 @@ import { TestCaseName, createRule, getNodeName, - isDescribe, + isDescribeCall, isExpectCall, isFunction, - isTestCase, + isTestCaseCall, } from './utils'; const getBlockType = ( @@ -39,7 +39,7 @@ const getBlockType = ( } // if it's not a variable, it will be callExpr, we only care about describe - if (expr.type === AST_NODE_TYPES.CallExpression && isDescribe(expr)) { + if (expr.type === AST_NODE_TYPES.CallExpression && isDescribeCall(expr)) { return 'describe'; } } @@ -94,7 +94,7 @@ export default createRule< additionalTestBlockFunctions.includes(getNodeName(node) || ''); const isTestBlock = (node: TSESTree.CallExpression): boolean => - isTestCase(node) || isCustomTestBlockFunction(node); + isTestCaseCall(node) || isCustomTestBlockFunction(node); return { CallExpression(node) { diff --git a/src/rules/no-test-prefixes.ts b/src/rules/no-test-prefixes.ts index 0c61dedfe..f708aa4c0 100644 --- a/src/rules/no-test-prefixes.ts +++ b/src/rules/no-test-prefixes.ts @@ -1,5 +1,10 @@ import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; -import { createRule, getNodeName, isDescribe, isTestCase } from './utils'; +import { + createRule, + getNodeName, + isDescribeCall, + isTestCaseCall, +} from './utils'; export default createRule({ name: __filename, @@ -22,7 +27,8 @@ export default createRule({ CallExpression(node) { const nodeName = getNodeName(node.callee); - if (!nodeName || (!isDescribe(node) && !isTestCase(node))) return; + if (!nodeName || (!isDescribeCall(node) && !isTestCaseCall(node))) + return; const preferredNodeName = getPreferredNodeName(nodeName); diff --git a/src/rules/no-test-return-statement.ts b/src/rules/no-test-return-statement.ts index 0f26398e3..26fa36e61 100644 --- a/src/rules/no-test-return-statement.ts +++ b/src/rules/no-test-return-statement.ts @@ -6,7 +6,7 @@ import { createRule, getTestCallExpressionsFromDeclaredVariables, isFunction, - isTestCase, + isTestCaseCall, } from './utils'; const getBody = (args: TSESTree.Expression[]) => { @@ -41,7 +41,7 @@ export default createRule({ create(context) { return { CallExpression(node) { - if (!isTestCase(node)) return; + if (!isTestCaseCall(node)) return; const body = getBody(node.arguments); const returnStmt = body.find( t => t.type === AST_NODE_TYPES.ReturnStatement, diff --git a/src/rules/no-try-expect.ts b/src/rules/no-try-expect.ts index ff209a80f..57647bf2a 100644 --- a/src/rules/no-try-expect.ts +++ b/src/rules/no-try-expect.ts @@ -3,7 +3,7 @@ import { createRule, getTestCallExpressionsFromDeclaredVariables, isExpectCall, - isTestCase, + isTestCaseCall, } from './utils'; export default createRule({ @@ -37,7 +37,7 @@ export default createRule({ return { CallExpression(node) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { isTest = true; } else if (isTest && isThrowExpectCall(node)) { context.report({ @@ -67,7 +67,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { isTest = false; } }, diff --git a/src/rules/prefer-hooks-on-top.ts b/src/rules/prefer-hooks-on-top.ts index 5d1e0ae61..1a393a319 100644 --- a/src/rules/prefer-hooks-on-top.ts +++ b/src/rules/prefer-hooks-on-top.ts @@ -1,4 +1,4 @@ -import { createRule, isHook, isTestCase } from './utils'; +import { createRule, isHook, isTestCaseCall } from './utils'; export default createRule({ name: __filename, @@ -20,7 +20,7 @@ export default createRule({ return { CallExpression(node) { - if (!isHook(node) && isTestCase(node)) { + if (!isHook(node) && isTestCaseCall(node)) { hooksContext[hooksContext.length - 1] = true; } if (hooksContext[hooksContext.length - 1] && isHook(node)) { diff --git a/src/rules/prefer-todo.ts b/src/rules/prefer-todo.ts index 6ba4f2d02..8bdd8dff5 100644 --- a/src/rules/prefer-todo.ts +++ b/src/rules/prefer-todo.ts @@ -11,7 +11,7 @@ import { hasOnlyOneArgument, isFunction, isStringNode, - isTestCase, + isTestCaseCall, } from './utils'; function isEmptyFunction(node: TSESTree.Expression) { @@ -36,7 +36,7 @@ function createTodoFixer( const isTargetedTestCase = ( node: TSESTree.CallExpression, ): node is JestFunctionCallExpression => - isTestCase(node) && + isTestCaseCall(node) && [TestCaseName.it, TestCaseName.test, 'it.skip', 'test.skip'].includes( getNodeName(node.callee), ); diff --git a/src/rules/require-top-level-describe.ts b/src/rules/require-top-level-describe.ts index f9c95ffea..12cb047aa 100644 --- a/src/rules/require-top-level-describe.ts +++ b/src/rules/require-top-level-describe.ts @@ -1,10 +1,10 @@ import { TSESTree } from '@typescript-eslint/experimental-utils'; import { createRule, - isDescribe, + isDescribeCall, isEachCall, isHook, - isTestCase, + isTestCaseCall, } from './utils'; export default createRule({ @@ -29,14 +29,14 @@ export default createRule({ return { CallExpression(node) { - if (isDescribe(node)) { + if (isDescribeCall(node)) { numberOfDescribeBlocks++; return; } if (numberOfDescribeBlocks === 0) { - if (isTestCase(node)) { + if (isTestCaseCall(node)) { context.report({ node, messageId: 'unexpectedTestCase' }); return; @@ -50,7 +50,7 @@ export default createRule({ } }, 'CallExpression:exit'(node: TSESTree.CallExpression) { - if (isDescribe(node) && !isEachCall(node)) { + if (isDescribeCall(node) && !isEachCall(node)) { numberOfDescribeBlocks--; } }, diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 77c4f0e93..cbfbba5ea 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -648,33 +648,127 @@ export const getTestCallExpressionsFromDeclaredVariables = ( (node): node is JestFunctionCallExpression => !!node && node.type === AST_NODE_TYPES.CallExpression && - isTestCase(node), + isTestCaseCall(node), ), ), [], ); }; -export const isTestCase = ( +const isTestCaseName = (node: TSESTree.LeftHandSideExpression) => + node.type === AST_NODE_TYPES.Identifier && + TestCaseName.hasOwnProperty(node.name); + +const isTestCaseProperty = ( + node: TSESTree.Expression, +): node is AccessorNode => + isSupportedAccessor(node) && + TestCaseProperty.hasOwnProperty(getAccessorValue(node)); + +/** + * Checks if the given `node` is a *call* to a test case function that would + * result in tests being run by `jest`. + * + * Note that `.each()` does not count as a call in this context, as it will not + * result in `jest` running any tests. + * + * @param {TSESTree.CallExpression} node + * + * @return {node is JestFunctionCallExpression} + */ +export const isTestCaseCall = ( node: TSESTree.CallExpression, -): node is JestFunctionCallExpression => - (node.callee.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.name)) || - // e.g. it.each``() - (node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression && +): node is JestFunctionCallExpression => { + if (isTestCaseName(node.callee)) { + return true; + } + + if ( + node.callee.type === AST_NODE_TYPES.MemberExpression && + isTestCaseProperty(node.callee.property) + ) { + // if we're an `each`, ensure we're being called (i.e `.each()()`) + if ( + getAccessorValue(node.callee.property) === 'each' && + node.parent?.type !== AST_NODE_TYPES.CallExpression + ) { + return false; + } + + return node.callee.object.type === AST_NODE_TYPES.MemberExpression + ? isTestCaseName(node.callee.object.object) + : isTestCaseName(node.callee.object); + } + + if ( + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression && + node.callee.tag.type === AST_NODE_TYPES.MemberExpression + ) { + return node.callee.tag.object.type === AST_NODE_TYPES.MemberExpression + ? isTestCaseName(node.callee.tag.object.object) + : isTestCaseName(node.callee.tag.object); + } + + return false; +}; + +const isDescribeAlias = (node: TSESTree.LeftHandSideExpression) => + node.type === AST_NODE_TYPES.Identifier && + DescribeAlias.hasOwnProperty(node.name); + +const isDescribeProperty = ( + node: TSESTree.Expression, +): node is AccessorNode => + isSupportedAccessor(node) && + DescribeProperty.hasOwnProperty(getAccessorValue(node)); + +/** + * Checks if the given `node` is a *call* to a `describe` function that would + * result in a `describe` block being created by `jest`. + * + * Note that `.each()` does not count as a call in this context, as it will not + * result in `jest` creating any `describe` blocks. + * + * @param {TSESTree.CallExpression} node + * + * @return {node is JestFunctionCallExpression} + */ +export const isDescribeCall = ( + node: TSESTree.CallExpression, +): node is JestFunctionCallExpression => { + if (isDescribeAlias(node.callee)) { + return true; + } + + if ( + node.callee.type === AST_NODE_TYPES.MemberExpression && + isDescribeProperty(node.callee.property) + ) { + // if we're an `each`, ensure we're being called (i.e `.each()()`) + if ( + getAccessorValue(node.callee.property) === 'each' && + node.parent?.type !== AST_NODE_TYPES.CallExpression + ) { + return false; + } + + return node.callee.object.type === AST_NODE_TYPES.MemberExpression + ? isDescribeAlias(node.callee.object.object) + : isDescribeAlias(node.callee.object); + } + + if ( + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression && node.callee.tag.type === AST_NODE_TYPES.MemberExpression && - node.callee.tag.object.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.tag.object.name) && - isSupportedAccessor(node.callee.tag.property, TestCaseProperty.each)) || - // e.g. it.concurrent.{skip,only} - (node.callee.type === AST_NODE_TYPES.MemberExpression && - node.callee.property.type === AST_NODE_TYPES.Identifier && - TestCaseProperty.hasOwnProperty(node.callee.property.name) && - ((node.callee.object.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.object.name)) || - (node.callee.object.type === AST_NODE_TYPES.MemberExpression && - node.callee.object.object.type === AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.callee.object.object.name)))); + isDescribeProperty(node.callee.tag.property) + ) { + return node.callee.tag.object.type === AST_NODE_TYPES.MemberExpression + ? isDescribeAlias(node.callee.tag.object.object) + : isDescribeAlias(node.callee.tag.object); + } + + return false; +}; export const isDescribe = ( node: TSESTree.CallExpression, diff --git a/src/rules/valid-title.ts b/src/rules/valid-title.ts index c970d0fbf..f92c5e3a1 100644 --- a/src/rules/valid-title.ts +++ b/src/rules/valid-title.ts @@ -10,9 +10,9 @@ import { getJestFunctionArguments, getNodeName, getStringValue, - isDescribe, + isDescribeCall, isStringNode, - isTestCase, + isTestCaseCall, } from './utils'; const trimFXprefix = (word: string) => @@ -161,7 +161,7 @@ export default createRule<[Options], MessageIds>({ return { CallExpression(node: TSESTree.CallExpression) { - if (!isDescribe(node) && !isTestCase(node)) { + if (!isDescribeCall(node) && !isTestCaseCall(node)) { return; } @@ -181,7 +181,7 @@ export default createRule<[Options], MessageIds>({ if ( argument.type !== AST_NODE_TYPES.TemplateLiteral && - !(ignoreTypeOfDescribeName && isDescribe(node)) + !(ignoreTypeOfDescribeName && isDescribeCall(node)) ) { context.report({ messageId: 'titleMustBeString', @@ -198,7 +198,7 @@ export default createRule<[Options], MessageIds>({ context.report({ messageId: 'emptyTitle', data: { - jestFunctionName: isDescribe(node) + jestFunctionName: isDescribeCall(node) ? DescribeAlias.describe : TestCaseName.test, },