diff --git a/src/rules/__tests__/consistent-test-it.test.ts b/src/rules/__tests__/consistent-test-it.test.ts index 9fc950da3..028177b56 100644 --- a/src/rules/__tests__/consistent-test-it.test.ts +++ b/src/rules/__tests__/consistent-test-it.test.ts @@ -213,6 +213,32 @@ ruleTester.run('consistent-test-it with fn=test', rule, { }, ], }, + { + code: dedent` + describe.only.each()("%s", () => { + test("is valid, but should not be", () => {}); + + it("is not valid, but should be", () => {}); + }); + `, + output: dedent` + describe.only.each()("%s", () => { + it("is valid, but should not be", () => {}); + + it("is not valid, but should be", () => {}); + }); + `, + options: [{ fn: TestCaseName.test, withinDescribe: TestCaseName.it }], + errors: [ + { + messageId: 'consistentMethodWithinDescribe', + data: { + testKeywordWithinDescribe: TestCaseName.it, + oppositeTestKeyword: TestCaseName.test, + }, + }, + ], + }, { code: 'describe("suite", () => { it("foo") })', output: 'describe("suite", () => { test("foo") })', diff --git a/src/rules/__tests__/lowercase-name.test.ts b/src/rules/__tests__/lowercase-name.test.ts index 9c608ea0d..31fbbf54c 100644 --- a/src/rules/__tests__/lowercase-name.test.ts +++ b/src/rules/__tests__/lowercase-name.test.ts @@ -52,6 +52,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__/no-conditional-expect.test.ts b/src/rules/__tests__/no-conditional-expect.test.ts index 1b294ea8e..b611d33fb 100644 --- a/src/rules/__tests__/no-conditional-expect.test.ts +++ b/src/rules/__tests__/no-conditional-expect.test.ts @@ -143,6 +143,14 @@ ruleTester.run('logical conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each\`\`('foo', () => { + something || expect(something).toHaveBeenCalled(); + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -202,6 +210,14 @@ ruleTester.run('conditional conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each\`\`('foo', () => { + something ? noop() : expect(something).toHaveBeenCalled(); + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -275,6 +291,19 @@ ruleTester.run('switch conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each\`\`('foo', () => { + switch(something) { + case 'value': + expect(something).toHaveBeenCalled(); + default: + break; + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -343,6 +372,18 @@ ruleTester.run('if conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each\`\`('foo', () => { + if(!doSomething) { + // do nothing + } else { + expect(something).toHaveBeenCalled(); + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { @@ -438,6 +479,30 @@ ruleTester.run('catch conditions', rule, { `, errors: [{ messageId: 'conditionalExpect' }], }, + { + code: ` + it.each\`\`('foo', () => { + try { + + } catch (err) { + expect(err).toMatch('Error'); + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, + { + code: ` + it.skip.each\`\`('foo', () => { + try { + + } catch (err) { + expect(err).toMatch('Error'); + } + }) + `, + errors: [{ messageId: 'conditionalExpect' }], + }, { code: ` function getValue() { diff --git a/src/rules/__tests__/no-export.test.ts b/src/rules/__tests__/no-export.test.ts index 3e56e91f4..e6cc6f356 100644 --- a/src/rules/__tests__/no-export.test.ts +++ b/src/rules/__tests__/no-export.test.ts @@ -1,4 +1,5 @@ import { TSESLint } from '@typescript-eslint/experimental-utils'; +import dedent from 'dedent'; import resolveFrom from 'resolve-from'; import rule from '../no-export'; @@ -23,7 +24,40 @@ ruleTester.run('no-export', rule, { invalid: [ { code: - 'export const myThing = "invalid"; test("a test", () => { expect(1).toBe(1);});', + 'export const myThing = "invalid"; test("a test", () => { expect(1).toBe(1);});', + parserOptions: { sourceType: 'module' }, + errors: [{ endColumn: 34, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: dedent` + export const myThing = 'invalid'; + + test.each()('my code', () => { + expect(1).toBe(1); + }); + `, + parserOptions: { sourceType: 'module' }, + errors: [{ endColumn: 34, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: dedent` + export const myThing = 'invalid'; + + test.each\`\`('my code', () => { + expect(1).toBe(1); + }); + `, + parserOptions: { sourceType: 'module' }, + errors: [{ endColumn: 34, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: dedent` + export const myThing = 'invalid'; + + test.only.each\`\`('my code', () => { + expect(1).toBe(1); + }); + `, parserOptions: { sourceType: 'module' }, errors: [{ endColumn: 34, column: 1, messageId: 'unexpectedExport' }], }, diff --git a/src/rules/__tests__/no-if.test.ts b/src/rules/__tests__/no-if.test.ts index 5b91441ab..d6554638e 100644 --- a/src/rules/__tests__/no-if.test.ts +++ b/src/rules/__tests__/no-if.test.ts @@ -812,6 +812,34 @@ ruleTester.run('if statements', rule, { }, ], }, + { + code: dedent` + it.each\`\`('foo', () => { + callExpression() + if ('bar') {} + }) + `, + errors: [ + { + data: { condition: 'if' }, + messageId: 'conditionalInTest', + }, + ], + }, + { + code: dedent` + it.only.each\`\`('foo', () => { + callExpression() + if ('bar') {} + }) + `, + errors: [ + { + data: { condition: 'if' }, + messageId: 'conditionalInTest', + }, + ], + }, { code: dedent` describe('valid', () => { diff --git a/src/rules/__tests__/no-test-return-statement.test.ts b/src/rules/__tests__/no-test-return-statement.test.ts index 5959e03ea..686c078f4 100644 --- a/src/rules/__tests__/no-test-return-statement.test.ts +++ b/src/rules/__tests__/no-test-return-statement.test.ts @@ -52,6 +52,30 @@ ruleTester.run('no-test-return-statement', rule, { `, errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], }, + { + code: dedent` + it.skip("one", function () { + return expect(1).toBe(1); + }); + `, + errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], + }, + { + code: dedent` + it.each\`\`("one", function () { + return expect(1).toBe(1); + }); + `, + errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], + }, + { + code: dedent` + it.only.each\`\`("one", function () { + return expect(1).toBe(1); + }); + `, + errors: [{ messageId: 'noReturnValue', column: 3, line: 2 }], + }, { code: dedent` it("one", myTest); diff --git a/src/rules/__tests__/prefer-hooks-on-top.test.ts b/src/rules/__tests__/prefer-hooks-on-top.test.ts index e5cfa4209..d7ba071be 100644 --- a/src/rules/__tests__/prefer-hooks-on-top.test.ts +++ b/src/rules/__tests__/prefer-hooks-on-top.test.ts @@ -45,6 +45,48 @@ ruleTester.run('basic describe block', rule, { }, ], }, + { + code: dedent` + describe('foo', () => { + beforeEach(() => {}); + test.each\`\`('bar', () => { + someFn(); + }); + beforeAll(() => {}); + test.only('bar', () => { + someFn(); + }); + }); + `, + errors: [ + { + messageId: 'noHookOnTop', + column: 3, + line: 6, + }, + ], + }, + { + code: dedent` + describe('foo', () => { + beforeEach(() => {}); + test.only.each\`\`('bar', () => { + someFn(); + }); + beforeAll(() => {}); + test.only('bar', () => { + someFn(); + }); + }); + `, + errors: [ + { + messageId: 'noHookOnTop', + column: 3, + line: 6, + }, + ], + }, ], }); diff --git a/src/rules/__tests__/require-top-level-describe.test.ts b/src/rules/__tests__/require-top-level-describe.test.ts index 26c218e84..b97d20a0b 100644 --- a/src/rules/__tests__/require-top-level-describe.test.ts +++ b/src/rules/__tests__/require-top-level-describe.test.ts @@ -12,6 +12,7 @@ const ruleTester = new TSESLint.RuleTester({ ruleTester.run('require-top-level-describe', rule, { valid: [ + 'it.each()', 'describe("test suite", () => { test("my test") });', 'describe("test suite", () => { it("my test") });', dedent` @@ -89,9 +90,25 @@ ruleTester.run('require-top-level-describe', rule, { `, errors: [{ messageId: 'unexpectedHook' }], }, + { + code: "it.skip('test', () => {});", + errors: [{ messageId: 'unexpectedTestCase' }], + }, { code: "it.each([1, 2, 3])('%n', () => {});", errors: [{ messageId: 'unexpectedTestCase' }], }, + { + code: "it.skip.each([1, 2, 3])('%n', () => {});", + errors: [{ messageId: 'unexpectedTestCase' }], + }, + { + code: "it.skip.each``('%n', () => {});", + errors: [{ messageId: 'unexpectedTestCase' }], + }, + { + code: "it.each``('%n', () => {});", + errors: [{ messageId: 'unexpectedTestCase' }], + }, ], }); diff --git a/src/rules/__tests__/utils.test.ts b/src/rules/__tests__/utils.test.ts new file mode 100644 index 000000000..ead3ebfea --- /dev/null +++ b/src/rules/__tests__/utils.test.ts @@ -0,0 +1,268 @@ +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: { + details: [ + 'callType', // + ] + .map(data => `${data}: {{ ${data} }}`) + .join('\n'), + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create: context => ({ + CallExpression(node) { + const callType = + (isDescribeCall(node) && ('describe' as const)) || + (isTestCaseCall(node) && ('test' as const)); + + if (callType) { + context.report({ + messageId: 'details', + node, + data: { callType }, + }); + } + }, + }), +}); + +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: [], +}); + +/** + * Tests the AST utils against the given member expressions both + * as is and as call expressions. + * + * @param {string[]} memberExpressions + * @param {"describe" | "test"} callType + * @param {boolean} skip + */ +const testUtilsAgainst = ( + memberExpressions: string[], + callType: 'describe' | 'test', + skip = false, +) => { + if (skip) { + return; + } + + ruleTester.run('it', rule, { + valid: memberExpressions, + invalid: memberExpressions.map(code => ({ + code: `${code}("works", () => {})`, + errors: [ + { + messageId: 'details' as const, + data: { callType }, + column: 1, + line: 1, + }, + ], + })), + }); +}; + +testUtilsAgainst( + [ + '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', + ], + 'test', +); + +testUtilsAgainst( + [ + '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', + ], + 'test', +); + +testUtilsAgainst( + [ + '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', + ], + 'describe', +); diff --git a/src/rules/__tests__/valid-title.test.ts b/src/rules/__tests__/valid-title.test.ts index 5d703bb9d..c0e30d2b7 100644 --- a/src/rules/__tests__/valid-title.test.ts +++ b/src/rules/__tests__/valid-title.test.ts @@ -353,6 +353,26 @@ ruleTester.run('title-must-be-string', rule, { }, ], }, + { + code: 'it.skip.each([])(1, () => {});', + errors: [ + { + messageId: 'titleMustBeString', + column: 18, + line: 1, + }, + ], + }, + { + code: 'it.skip.each``(1, () => {});', + errors: [ + { + messageId: 'titleMustBeString', + column: 16, + line: 1, + }, + ], + }, { code: 'it(123, () => {});', errors: [ @@ -658,6 +678,16 @@ ruleTester.run('no-accidental-space', rule, { output: 'describe("foo", function () {})', errors: [{ messageId: 'accidentalSpace', column: 10, line: 1 }], }, + { + code: 'describe.each()(" foo", function () {})', + output: 'describe.each()("foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 17, line: 1 }], + }, + { + code: 'describe.only.each()(" foo", function () {})', + output: 'describe.only.each()("foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 22, line: 1 }], + }, { code: 'describe(" foo foe fum", function () {})', output: 'describe("foo foe fum", function () {})', diff --git a/src/rules/consistent-test-it.ts b/src/rules/consistent-test-it.ts index dc6c8c12b..6675df48e 100644 --- a/src/rules/consistent-test-it.ts +++ b/src/rules/consistent-test-it.ts @@ -7,9 +7,9 @@ import { TestCaseName, createRule, getNodeName, - isDescribe, + isDescribeCall, isEachCall, - isTestCase, + isTestCaseCall, } from './utils'; const buildFixer = ( @@ -79,7 +79,7 @@ export default createRule< return; } - if (isDescribe(node)) { + if (isDescribeCall(node)) { describeNestingLevel++; } @@ -89,7 +89,7 @@ export default createRule< : node.callee; if ( - isTestCase(node) && + isTestCaseCall(node) && describeNestingLevel === 0 && !nodeName.includes(testKeyword) ) { @@ -104,7 +104,7 @@ export default createRule< } if ( - isTestCase(node) && + isTestCaseCall(node) && describeNestingLevel > 0 && !nodeName.includes(testKeywordWithinDescribe) ) { @@ -121,7 +121,7 @@ export default createRule< } }, 'CallExpression:exit'(node) { - if (isDescribe(node) && !isEachCall(node)) { + if (isDescribeCall(node) && !isEachCall(node)) { describeNestingLevel--; } }, diff --git a/src/rules/lowercase-name.ts b/src/rules/lowercase-name.ts index a083c29b4..ce2c78644 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 3a699d010..d1aaecaf2 100644 --- a/src/rules/no-duplicate-hooks.ts +++ b/src/rules/no-duplicate-hooks.ts @@ -1,4 +1,4 @@ -import { createRule, isDescribe, isEachCall, isHook } from './utils'; +import { createRule, isDescribeCall, isEachCall, 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) && !isEachCall(node)) { + if (isDescribeCall(node) && !isEachCall(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-expect-assertions.ts b/src/rules/prefer-expect-assertions.ts index d62d76dfd..0a3ce8342 100644 --- a/src/rules/prefer-expect-assertions.ts +++ b/src/rules/prefer-expect-assertions.ts @@ -11,7 +11,7 @@ import { isEachCall, isFunction, isSupportedAccessor, - isTestCase, + isTestCaseCall, } from './utils'; const isExpectAssertionsOrHasAssertionsCall = ( @@ -101,7 +101,7 @@ export default createRule<[RuleOptions], MessageIds>({ create(context, [options]) { return { CallExpression(node: TSESTree.CallExpression) { - if (!isTestCase(node)) { + if (!isTestCaseCall(node)) { return; } 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 1e701da7b..b2c904cdf 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -648,33 +648,120 @@ 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.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)))); +): node is JestFunctionCallExpression => { + if (isTestCaseName(node.callee)) { + return true; + } + + const callee = + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression + ? node.callee.tag + : node.callee; + + if ( + callee.type === AST_NODE_TYPES.MemberExpression && + isTestCaseProperty(callee.property) + ) { + // if we're an `each()`, ensure we're being called (i.e `.each()()`) + if ( + node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression && + node.parent?.type !== AST_NODE_TYPES.CallExpression && + getAccessorValue(callee.property) === 'each' + ) { + return false; + } + + return callee.object.type === AST_NODE_TYPES.MemberExpression + ? isTestCaseName(callee.object.object) + : isTestCaseName(callee.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; + } + + const callee = + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression + ? node.callee.tag + : node.callee; + + if ( + callee.type === AST_NODE_TYPES.MemberExpression && + isDescribeProperty(callee.property) + ) { + // if we're an `each()`, ensure we're being called (i.e `.each()()`) + if ( + node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression && + node.parent?.type !== AST_NODE_TYPES.CallExpression && + getAccessorValue(callee.property) === 'each' + ) { + return false; + } + + return callee.object.type === AST_NODE_TYPES.MemberExpression + ? isDescribeAlias(callee.object.object) + : isDescribeAlias(callee.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, },