From c8ce37a6da61d4aad4a8e8253030c76b60d6a60e Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Tue, 27 Oct 2020 18:26:47 +1300 Subject: [PATCH 1/5] fix(no-disabled-tests): fix bug with it.each --- src/rules/__tests__/no-disabled-tests.test.ts | 33 +++++++++++++++++++ src/rules/no-disabled-tests.ts | 7 ++++ src/rules/utils.ts | 2 ++ 3 files changed, 42 insertions(+) diff --git a/src/rules/__tests__/no-disabled-tests.test.ts b/src/rules/__tests__/no-disabled-tests.test.ts index 3f317c0be..34a847698 100644 --- a/src/rules/__tests__/no-disabled-tests.test.ts +++ b/src/rules/__tests__/no-disabled-tests.test.ts @@ -16,6 +16,7 @@ ruleTester.run('no-disabled-tests', rule, { 'it("foo", function () {})', 'describe.only("foo", function () {})', 'it.only("foo", function () {})', + 'it.each("foo", () => {})', 'it.concurrent("foo", function () {})', 'test("foo", function () {})', 'test.only("foo", function () {})', @@ -87,6 +88,22 @@ ruleTester.run('no-disabled-tests', rule, { code: 'test.skip("foo", function () {})', errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], }, + { + code: 'it.skip.each``("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, + { + code: 'test.skip.each``("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, + { + code: 'it.skip.each([])("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, + { + code: 'test.skip.each([])("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, { code: 'test.concurrent.skip("foo", function () {})', errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], @@ -107,6 +124,22 @@ ruleTester.run('no-disabled-tests', rule, { code: 'xtest("foo", function () {})', errors: [{ messageId: 'disabledTest', column: 1, line: 1 }], }, + { + code: 'xit.each``("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, + { + code: 'xtest.each``("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, + { + code: 'xit.each([])("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, + { + code: 'xtest.each([])("foo", function () {})', + errors: [{ messageId: 'skippedTest', column: 1, line: 1 }], + }, { code: 'it("has title but no callback")', errors: [{ messageId: 'missingFunction', column: 1, line: 1 }], diff --git a/src/rules/no-disabled-tests.ts b/src/rules/no-disabled-tests.ts index 53f6233e1..dc5cb6fc5 100644 --- a/src/rules/no-disabled-tests.ts +++ b/src/rules/no-disabled-tests.ts @@ -39,6 +39,9 @@ export default createRule({ CallExpression(node) { const functionName = getNodeName(node.callee); + // prevent duplicate warnings for it.each()() + if (node.callee.type === 'CallExpression') return; + switch (functionName) { case 'describe.skip': context.report({ messageId: 'skippedTestSuite', node }); @@ -48,6 +51,10 @@ export default createRule({ case 'it.concurrent.skip': case 'test.skip': case 'test.concurrent.skip': + case 'it.skip.each': + case 'test.skip.each': + case 'xit.each': + case 'xtest.each': context.report({ messageId: 'skippedTest', node }); break; } diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 107675d81..4d95d9f63 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -601,6 +601,8 @@ export function getNodeName(node: TSESTree.Node): string | null { } switch (node.type) { + case AST_NODE_TYPES.TaggedTemplateExpression: + return getNodeName(node.tag); case AST_NODE_TYPES.MemberExpression: return joinNames(getNodeName(node.object), getNodeName(node.property)); case AST_NODE_TYPES.NewExpression: From 00d9b8d64206d4160f4eb9464340a49ab27c6ef2 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Tue, 27 Oct 2020 19:48:30 +1300 Subject: [PATCH 2/5] fix(no-test-prefixes): fix bug with it.each --- src/rules/__tests__/no-test-prefixes.test.ts | 60 ++++++++++++++++++++ src/rules/no-test-prefixes.ts | 14 ++++- src/rules/utils.ts | 12 ++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/rules/__tests__/no-test-prefixes.test.ts b/src/rules/__tests__/no-test-prefixes.test.ts index 330ad0aaa..731fe74fa 100644 --- a/src/rules/__tests__/no-test-prefixes.test.ts +++ b/src/rules/__tests__/no-test-prefixes.test.ts @@ -12,8 +12,18 @@ ruleTester.run('no-test-prefixes', rule, { 'test.concurrent("foo", function () {})', 'describe.only("foo", function () {})', 'it.only("foo", function () {})', + 'it.each()("foo", function () {})', + { + code: 'it.each``("foo", function () {})', + parserOptions: { ecmaVersion: 6 }, + }, 'it.concurrent.only("foo", function () {})', 'test.only("foo", function () {})', + 'test.each()("foo", function () {})', + { + code: 'test.each``("foo", function () {})', + parserOptions: { ecmaVersion: 6 }, + }, 'test.concurrent.only("foo", function () {})', 'describe.skip("foo", function () {})', 'it.skip("foo", function () {})', @@ -96,5 +106,55 @@ ruleTester.run('no-test-prefixes', rule, { }, ], }, + { + code: 'xit.each``("foo", function () {})', + output: 'it.skip.each``("foo", function () {})', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'usePreferredName', + data: { preferredNodeName: 'it.skip.each' }, + column: 1, + line: 1, + }, + ], + }, + { + code: 'xtest.each``("foo", function () {})', + output: 'test.skip.each``("foo", function () {})', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'usePreferredName', + data: { preferredNodeName: 'test.skip.each' }, + column: 1, + line: 1, + }, + ], + }, + { + code: 'xit.each([])("foo", function () {})', + output: 'it.skip.each([])("foo", function () {})', + errors: [ + { + messageId: 'usePreferredName', + data: { preferredNodeName: 'it.skip.each' }, + column: 1, + line: 1, + }, + ], + }, + { + code: 'xtest.each([])("foo", function () {})', + output: 'test.skip.each([])("foo", function () {})', + errors: [ + { + messageId: 'usePreferredName', + data: { preferredNodeName: 'test.skip.each' }, + column: 1, + line: 1, + }, + ], + }, ], }); diff --git a/src/rules/no-test-prefixes.ts b/src/rules/no-test-prefixes.ts index 1874a06b8..0c61dedfe 100644 --- a/src/rules/no-test-prefixes.ts +++ b/src/rules/no-test-prefixes.ts @@ -1,3 +1,4 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import { createRule, getNodeName, isDescribe, isTestCase } from './utils'; export default createRule({ @@ -27,12 +28,17 @@ export default createRule({ if (!preferredNodeName) return; + const funcNode = + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression + ? node.callee.tag + : node.callee; + context.report({ messageId: 'usePreferredName', node: node.callee, data: { preferredNodeName }, fix(fixer) { - return [fixer.replaceText(node.callee, preferredNodeName)]; + return [fixer.replaceText(funcNode, preferredNodeName)]; }, }); }, @@ -43,12 +49,14 @@ export default createRule({ function getPreferredNodeName(nodeName: string) { const firstChar = nodeName.charAt(0); + const suffix = nodeName.endsWith('.each') ? '.each' : ''; + if (firstChar === 'f') { - return `${nodeName.slice(1)}.only`; + return `${nodeName.slice(1).replace('.each', '')}.only${suffix}`; } if (firstChar === 'x') { - return `${nodeName.slice(1)}.skip`; + return `${nodeName.slice(1).replace('.each', '')}.skip${suffix}`; } return null; diff --git a/src/rules/utils.ts b/src/rules/utils.ts index 4d95d9f63..f373ac4e9 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -580,10 +580,16 @@ export interface JestFunctionCallExpressionWithIdentifierCallee< callee: JestFunctionIdentifier; } +export interface JestFunctionCallTaggedTemplateExpression + extends TSESTree.CallExpression { + callee: TSESTree.TaggedTemplateExpression; +} + export type JestFunctionCallExpression< FunctionName extends JestFunctionName = JestFunctionName > = | JestFunctionCallExpressionWithMemberExpressionCallee + | JestFunctionCallTaggedTemplateExpression | JestFunctionCallExpressionWithIdentifierCallee; const joinNames = (a: string | null, b: string | null): string | null => @@ -592,6 +598,7 @@ const joinNames = (a: string | null, b: string | null): string | null => export function getNodeName( node: | JestFunctionMemberExpression + | TSESTree.TaggedTemplateExpression | JestFunctionIdentifier, ): string; export function getNodeName(node: TSESTree.Node): string | null; @@ -653,6 +660,11 @@ export const isTestCase = ( ): 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 && + 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) && From 35684d52d1ac74a9fc6273edcbc8390350c2d094 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Wed, 28 Oct 2020 21:49:44 +1300 Subject: [PATCH 3/5] fix(consistent-test-it): fix bug with it.each --- .../__tests__/consistent-test-it.test.ts | 72 +++++++++++++++++++ src/rules/consistent-test-it.ts | 9 ++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/rules/__tests__/consistent-test-it.test.ts b/src/rules/__tests__/consistent-test-it.test.ts index 2c96ea310..75afdeef6 100644 --- a/src/rules/__tests__/consistent-test-it.test.ts +++ b/src/rules/__tests__/consistent-test-it.test.ts @@ -32,6 +32,14 @@ ruleTester.run('consistent-test-it with fn=test', rule, { code: 'xtest("foo")', options: [{ fn: TestCaseName.test }], }, + { + code: 'test.each([])("foo")', + options: [{ fn: TestCaseName.test }], + }, + { + code: 'test.each``("foo")', + options: [{ fn: TestCaseName.test }], + }, { code: 'describe("suite", () => { test("foo") })', options: [{ fn: TestCaseName.test }], @@ -122,6 +130,34 @@ ruleTester.run('consistent-test-it with fn=test', rule, { }, ], }, + { + code: 'it.each([])("foo")', + output: 'test.each([])("foo")', + options: [{ fn: TestCaseName.test }], + errors: [ + { + messageId: 'consistentMethod', + data: { + testKeyword: TestCaseName.test, + oppositeTestKeyword: TestCaseName.it, + }, + }, + ], + }, + { + code: 'it.each``("foo")', + output: 'test.each``("foo")', + options: [{ fn: TestCaseName.test }], + errors: [ + { + messageId: 'consistentMethod', + data: { + testKeyword: TestCaseName.test, + oppositeTestKeyword: TestCaseName.it, + }, + }, + ], + }, { code: 'describe("suite", () => { it("foo") })', output: 'describe("suite", () => { test("foo") })', @@ -165,6 +201,14 @@ ruleTester.run('consistent-test-it with fn=it', rule, { code: 'it.concurrent("foo")', options: [{ fn: TestCaseName.it }], }, + { + code: 'it.each([])("foo")', + options: [{ fn: TestCaseName.it }], + }, + { + code: 'it.each``("foo")', + options: [{ fn: TestCaseName.it }], + }, { code: 'describe("suite", () => { it("foo") })', options: [{ fn: TestCaseName.it }], @@ -241,6 +285,34 @@ ruleTester.run('consistent-test-it with fn=it', rule, { }, ], }, + { + code: 'test.each([])("foo")', + output: 'it.each([])("foo")', + options: [{ fn: TestCaseName.it }], + errors: [ + { + messageId: 'consistentMethod', + data: { + testKeyword: TestCaseName.it, + oppositeTestKeyword: TestCaseName.test, + }, + }, + ], + }, + { + code: 'test.each``("foo")', + output: 'it.each``("foo")', + options: [{ fn: TestCaseName.it }], + errors: [ + { + messageId: 'consistentMethod', + data: { + testKeyword: TestCaseName.it, + oppositeTestKeyword: TestCaseName.test, + }, + }, + ], + }, { code: 'describe("suite", () => { test("foo") })', output: 'describe("suite", () => { it("foo") })', diff --git a/src/rules/consistent-test-it.ts b/src/rules/consistent-test-it.ts index cfb204010..75cc9025e 100644 --- a/src/rules/consistent-test-it.ts +++ b/src/rules/consistent-test-it.ts @@ -82,6 +82,11 @@ export default createRule< describeNestingLevel++; } + const funcNode = + node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression + ? node.callee.tag + : node.callee; + if ( isTestCase(node) && describeNestingLevel === 0 && @@ -93,7 +98,7 @@ export default createRule< messageId: 'consistentMethod', node: node.callee, data: { testKeyword, oppositeTestKeyword }, - fix: buildFixer(node.callee, nodeName, testKeyword), + fix: buildFixer(funcNode, nodeName, testKeyword), }); } @@ -110,7 +115,7 @@ export default createRule< messageId: 'consistentMethodWithinDescribe', node: node.callee, data: { testKeywordWithinDescribe, oppositeTestKeyword }, - fix: buildFixer(node.callee, nodeName, testKeywordWithinDescribe), + fix: buildFixer(funcNode, nodeName, testKeywordWithinDescribe), }); } }, From e71a1e2435f756c44eaee303be6b978cbbf140ba Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Wed, 28 Oct 2020 22:29:02 +1300 Subject: [PATCH 4/5] fix(no-standalone-expect): coverage back to 100% --- src/rules/__tests__/no-standalone-expect.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rules/__tests__/no-standalone-expect.test.ts b/src/rules/__tests__/no-standalone-expect.test.ts index 7b4944545..cbe7bb9cf 100644 --- a/src/rules/__tests__/no-standalone-expect.test.ts +++ b/src/rules/__tests__/no-standalone-expect.test.ts @@ -57,6 +57,10 @@ ruleTester.run('no-standalone-expect', rule, { }, ], invalid: [ + { + code: `(() => {})('testing', () => expect(true))`, + errors: [{ endColumn: 41, column: 29, messageId: 'unexpectedExpect' }], + }, { code: ` describe('scenario', () => { From e0c06d839f27d4bb7f2f0703bca426dd828afbd4 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Sun, 1 Nov 2020 16:28:00 +1300 Subject: [PATCH 5/5] fix: code review changes --- src/rules/__tests__/no-standalone-expect.test.ts | 2 +- src/rules/utils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rules/__tests__/no-standalone-expect.test.ts b/src/rules/__tests__/no-standalone-expect.test.ts index cbe7bb9cf..7b60f524c 100644 --- a/src/rules/__tests__/no-standalone-expect.test.ts +++ b/src/rules/__tests__/no-standalone-expect.test.ts @@ -58,7 +58,7 @@ ruleTester.run('no-standalone-expect', rule, { ], invalid: [ { - code: `(() => {})('testing', () => expect(true))`, + code: "(() => {})('testing', () => expect(true))", errors: [{ endColumn: 41, column: 29, messageId: 'unexpectedExpect' }], }, { diff --git a/src/rules/utils.ts b/src/rules/utils.ts index f373ac4e9..6e487b4b1 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -580,7 +580,7 @@ export interface JestFunctionCallExpressionWithIdentifierCallee< callee: JestFunctionIdentifier; } -export interface JestFunctionCallTaggedTemplateExpression +interface JestFunctionCallExpressionWithTaggedTemplateCallee extends TSESTree.CallExpression { callee: TSESTree.TaggedTemplateExpression; } @@ -589,7 +589,7 @@ export type JestFunctionCallExpression< FunctionName extends JestFunctionName = JestFunctionName > = | JestFunctionCallExpressionWithMemberExpressionCallee - | JestFunctionCallTaggedTemplateExpression + | JestFunctionCallExpressionWithTaggedTemplateCallee | JestFunctionCallExpressionWithIdentifierCallee; const joinNames = (a: string | null, b: string | null): string | null => @@ -598,8 +598,8 @@ const joinNames = (a: string | null, b: string | null): string | null => export function getNodeName( node: | JestFunctionMemberExpression - | TSESTree.TaggedTemplateExpression - | JestFunctionIdentifier, + | JestFunctionIdentifier + | TSESTree.TaggedTemplateExpression, ): string; export function getNodeName(node: TSESTree.Node): string | null; export function getNodeName(node: TSESTree.Node): string | null {