diff --git a/lib/detect-testing-library-utils.ts b/lib/create-testing-library-rule/detect-testing-library-utils.ts similarity index 98% rename from lib/detect-testing-library-utils.ts rename to lib/create-testing-library-rule/detect-testing-library-utils.ts index 0c81fa6e..3d6662be 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/create-testing-library-rule/detect-testing-library-utils.ts @@ -3,6 +3,7 @@ import { TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; + import { getAssertNodeInfo, getDeepestIdentifierNode, @@ -19,13 +20,13 @@ import { isMemberExpression, isObjectPattern, isProperty, -} from './node-utils'; +} from '../node-utils'; import { ABSENCE_MATCHERS, ALL_QUERIES_COMBINATIONS, ASYNC_UTILS, PRESENCE_MATCHERS, -} from './utils'; +} from '../utils'; const SETTING_OPTION_OFF = 'off' as const; @@ -123,6 +124,17 @@ const FIRE_EVENT_NAME = 'fireEvent'; const USER_EVENT_NAME = 'userEvent'; const RENDER_NAME = 'render'; +export type DetectionOptions = { + /** + * If true, force `detectTestingLibraryUtils` to skip `canReportErrors` + * so it doesn't opt-out rule listener. + * + * Useful when some rule apply to files other than testing ones + * (e.g. `consistent-data-testid`) + */ + skipRuleReportingCheck: boolean; +}; + /** * Enhances a given rule `create` with helpers to detect Testing Library utils. */ @@ -130,7 +142,10 @@ export function detectTestingLibraryUtils< TOptions extends readonly unknown[], TMessageIds extends string, TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener ->(ruleCreate: EnhancedRuleCreate) { +>( + ruleCreate: EnhancedRuleCreate, + { skipRuleReportingCheck = false }: Partial = {} +) { return ( context: TestingLibraryContext, optionsWithDefault: Readonly @@ -742,7 +757,7 @@ export function detectTestingLibraryUtils< * Determines if file inspected meets all conditions to be reported by rules or not. */ const canReportErrors: CanReportErrorsFn = () => { - return isTestingLibraryImported(); + return skipRuleReportingCheck || isTestingLibraryImported(); }; /** diff --git a/lib/create-testing-library-rule.ts b/lib/create-testing-library-rule/index.ts similarity index 65% rename from lib/create-testing-library-rule.ts rename to lib/create-testing-library-rule/index.ts index 96f47d66..fda6ad26 100644 --- a/lib/create-testing-library-rule.ts +++ b/lib/create-testing-library-rule/index.ts @@ -1,6 +1,9 @@ import { ESLintUtils, TSESLint } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from './utils'; + +import { getDocsUrl } from '../utils'; + import { + DetectionOptions, detectTestingLibraryUtils, EnhancedRuleCreate, } from './detect-testing-library-utils'; @@ -15,20 +18,22 @@ export function createTestingLibraryRule< TOptions extends readonly unknown[], TMessageIds extends string, TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener ->( - config: Readonly<{ - name: string; - meta: CreateRuleMeta; - defaultOptions: Readonly; - create: EnhancedRuleCreate; - }> -): TSESLint.RuleModule { - const { create, ...remainingConfig } = config; - +>({ + create, + detectionOptions = {}, + ...remainingConfig +}: Readonly<{ + name: string; + meta: CreateRuleMeta; + defaultOptions: Readonly; + detectionOptions?: Partial; + create: EnhancedRuleCreate; +}>): TSESLint.RuleModule { return ESLintUtils.RuleCreator(getDocsUrl)({ ...remainingConfig, create: detectTestingLibraryUtils( - create + create, + detectionOptions ), }); } diff --git a/lib/rules/consistent-data-testid.ts b/lib/rules/consistent-data-testid.ts index f31a7b53..63a9b888 100644 --- a/lib/rules/consistent-data-testid.ts +++ b/lib/rules/consistent-data-testid.ts @@ -1,10 +1,9 @@ -import { getDocsUrl } from '../utils'; -import { ESLintUtils } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; import { isJSXAttribute, isLiteral } from '../node-utils'; export const RULE_NAME = 'consistent-data-testid'; export type MessageIds = 'consistentDataTestId'; -type Options = [ +export type Options = [ { testIdAttribute?: string | string[]; testIdPattern: string; @@ -13,12 +12,7 @@ type Options = [ const FILENAME_PLACEHOLDER = '{fileName}'; -/** - * This rule is not created with `createTestingLibraryRule` since: - * - it doesn't need any detection helper - * - it doesn't apply to testing files but component files - */ -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -64,8 +58,11 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ testIdAttribute: 'data-testid', }, ], + detectionOptions: { + skipRuleReportingCheck: true, + }, - create(context, [options]) { + create: (context, [options]) => { const { getFilename } = context; const { testIdPattern, testIdAttribute: attr } = options; diff --git a/tests/lib/rules/consistent-data-testid.test.ts b/tests/lib/rules/consistent-data-testid.test.ts index dd0892f2..4ea34f6c 100644 --- a/tests/lib/rules/consistent-data-testid.test.ts +++ b/tests/lib/rules/consistent-data-testid.test.ts @@ -1,12 +1,31 @@ +import type { TSESLint } from '@typescript-eslint/experimental-utils'; + +import rule, { + MessageIds, + Options, + RULE_NAME, +} from '../../../lib/rules/consistent-data-testid'; + import { createRuleTester } from '../test-utils'; -import rule, { RULE_NAME } from '../../../lib/rules/consistent-data-testid'; const ruleTester = createRuleTester(); -ruleTester.run(RULE_NAME, rule, { - valid: [ - { - code: ` +type ValidTestCase = TSESLint.ValidTestCase; +type InvalidTestCase = TSESLint.InvalidTestCase; +type TestCase = ValidTestCase | InvalidTestCase; +const disableAggressiveReporting = (array: T[]): T[] => + array.map((testCase) => ({ + ...testCase, + settings: { + 'testing-library/utils-module': 'off', + 'testing-library/custom-renders': 'off', + 'testing-library/custom-queries': 'off', + }, + })); + +const validTestCases: ValidTestCase[] = [ + { + code: ` import React from 'react'; const TestComponent = props => { @@ -17,10 +36,10 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [{ testIdPattern: 'cool' }], - }, - { - code: ` + options: [{ testIdPattern: 'cool' }], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -31,10 +50,10 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [{ testIdPattern: 'cool' }], - }, - { - code: ` + options: [{ testIdPattern: 'cool' }], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -45,15 +64,15 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/file/path/Awesome.js', - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/Awesome.js', + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -64,15 +83,15 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/file/path/Awesome.js', - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/path/Awesome.js', + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -83,15 +102,15 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/file/Parent/index.js', - }, - { - code: ` + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/file/Parent/index.js', + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -102,15 +121,15 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '{fileName}', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - }, - { - code: ` + options: [ + { + testIdPattern: '{fileName}', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -121,15 +140,15 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^right(.*)$', - testIdAttribute: 'custom-attr', - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: '^right(.*)$', + testIdAttribute: 'custom-attr', + }, + ], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -140,15 +159,15 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^right(.*)$', - testIdAttribute: ['custom-attr', 'another-custom-attr'], - }, - ], - }, - { - code: ` + options: [ + { + testIdPattern: '^right(.*)$', + testIdAttribute: ['custom-attr', 'another-custom-attr'], + }, + ], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -159,16 +178,16 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '{fileName}', - testIdAttribute: 'data-test-id', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - }, - { - code: ` + options: [ + { + testIdPattern: '{fileName}', + testIdAttribute: 'data-test-id', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -180,12 +199,12 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [{ testIdPattern: 'somethingElse' }], - }, - ], - invalid: [ - { - code: ` + options: [{ testIdPattern: 'somethingElse' }], + }, +]; +const invalidTestCases: InvalidTestCase[] = [ + { + code: ` import React from 'react'; const TestComponent = props => { @@ -196,20 +215,20 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [{ testIdPattern: 'error' }], - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'data-testid', - value: 'Awesome__CoolStuff', - regex: '/error/', - }, + options: [{ testIdPattern: 'error' }], + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'data-testid', + value: 'Awesome__CoolStuff', + regex: '/error/', }, - ], - }, - { - code: ` + }, + ], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -220,25 +239,25 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: 'matchMe', + options: [ + { + testIdPattern: 'matchMe', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'data-testid', + value: 'Nope', + regex: '/matchMe/', }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'data-testid', - value: 'Nope', - regex: '/matchMe/', - }, - }, - ], - }, - { - code: ` + }, + ], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -249,26 +268,26 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - testIdAttribute: 'my-custom-attr', + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + testIdAttribute: 'my-custom-attr', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'my-custom-attr', + value: 'WrongComponent__cool', + regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'my-custom-attr', - value: 'WrongComponent__cool', - regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', - }, - }, - ], - }, - { - code: ` + }, + ], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -279,34 +298,34 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^right$', - testIdAttribute: ['custom-attr', 'another-custom-attr'], - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'custom-attr', - value: 'wrong', - regex: '/^right$/', - }, + options: [ + { + testIdPattern: '^right$', + testIdAttribute: ['custom-attr', 'another-custom-attr'], + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'custom-attr', + value: 'wrong', + regex: '/^right$/', }, - { - messageId: 'consistentDataTestId', - data: { - attr: 'another-custom-attr', - value: 'wrong', - regex: '/^right$/', - }, + }, + { + messageId: 'consistentDataTestId', + data: { + attr: 'another-custom-attr', + value: 'wrong', + regex: '/^right$/', }, - ], - }, - { - code: ` + }, + ], + }, + { + code: ` import React from 'react'; const TestComponent = props => { @@ -317,22 +336,29 @@ ruleTester.run(RULE_NAME, rule, { ) }; `, - options: [ - { - testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', - }, - ], - filename: '/my/cool/__tests__/Parent/index.js', - errors: [ - { - messageId: 'consistentDataTestId', - data: { - attr: 'data-testid', - value: 'WrongComponent__cool', - regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', - }, + options: [ + { + testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$', + }, + ], + filename: '/my/cool/__tests__/Parent/index.js', + errors: [ + { + messageId: 'consistentDataTestId', + data: { + attr: 'data-testid', + value: 'WrongComponent__cool', + regex: '/^Parent(__([A-Z]+[a-z]*?)+)*$/', }, - ], - }, + }, + ], + }, +]; + +ruleTester.run(RULE_NAME, rule, { + valid: [...validTestCases, ...disableAggressiveReporting(validTestCases)], + invalid: [ + ...invalidTestCases, + ...disableAggressiveReporting(invalidTestCases), ], });