diff --git a/docs/rules/prefer-explicit-assert.md b/docs/rules/prefer-explicit-assert.md index b52a17e8..136c3b29 100644 --- a/docs/rules/prefer-explicit-assert.md +++ b/docs/rules/prefer-explicit-assert.md @@ -43,14 +43,11 @@ expect(queryByText('foo')).toBeInTheDocument(); await waitForElement(() => getByText('foo')); fireEvent.click(getByText('bar')); const quxElement = getByText('qux'); - -// call directly something different than Testing Library query -getByNonTestingLibraryVariant('foo'); ``` ## Options -This rule has a few options: +This rule has one option: - `assertion`: this string allows defining the preferred assertion to use with `getBy*` queries. By default, any assertion is valid (`toBeTruthy`, @@ -66,18 +63,9 @@ This rule has a few options: "testing-library/prefer-explicit-assert": ["error", {"assertion": "toBeInTheDocument"}], ``` -- `customQueryNames`: this array option allows to extend default Testing - Library queries with custom ones for including them into rule - inspection. - - ```js - "testing-library/prefer-explicit-assert": ["error", {"customQueryNames": ["getByIcon", "getBySomethingElse"]}], - ``` - ## When Not To Use It -If you prefer to use `getBy*` queries implicitly as an assert-like -method itself, then this rule is not recommended. +If you prefer to use `getBy*` queries implicitly as an assert-like method itself, then this rule is not recommended. ## Further Reading diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index f44ad477..98272538 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -1,16 +1,9 @@ -import { - ESLintUtils, - TSESTree, - ASTUtils, -} from '@typescript-eslint/experimental-utils'; -import { - getDocsUrl, - ALL_QUERIES_METHODS, - PRESENCE_MATCHERS, - ABSENCE_MATCHERS, -} from '../utils'; +import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; +import { PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils'; import { findClosestCallNode, isMemberExpression } from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; + export const RULE_NAME = 'prefer-explicit-assert'; export type MessageIds = | 'preferExplicitAssert' @@ -18,22 +11,13 @@ export type MessageIds = type Options = [ { assertion?: string; - customQueryNames?: string[]; } ]; -const ALL_GET_BY_QUERIES = ALL_QUERIES_METHODS.map( - (queryMethod) => `get${queryMethod}` -); - -const isValidQuery = (node: TSESTree.Identifier, customQueryNames: string[]) => - ALL_GET_BY_QUERIES.includes(node.name) || - customQueryNames.includes(node.name); - const isAtTopLevel = (node: TSESTree.Node) => node.parent.parent.type === 'ExpressionStatement'; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -59,26 +43,18 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ type: 'string', enum: PRESENCE_MATCHERS, }, - customQueryNames: { - type: 'array', - }, }, }, ], }, - defaultOptions: [ - { - customQueryNames: [], - }, - ], - - create: function (context, [options]) { - const { customQueryNames, assertion } = options; + defaultOptions: [{}], + create(context, [options], helpers) { + const { assertion } = options; const getQueryCalls: TSESTree.Identifier[] = []; return { 'CallExpression Identifier'(node: TSESTree.Identifier) { - if (isValidQuery(node, customQueryNames)) { + if (helpers.isGetByQuery(node)) { getQueryCalls.push(node); } }, @@ -93,7 +69,9 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ node: queryCall, messageId: 'preferExplicitAssert', }); - } else if (assertion) { + } + + if (assertion) { const expectCallNode = findClosestCallNode(node, 'expect'); if (!expectCallNode) return; diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index 7106b301..db8c664f 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -4,90 +4,102 @@ import { ALL_QUERIES_METHODS } from '../../../lib/utils'; const ruleTester = createRuleTester(); +const COMBINED_QUERIES_METHODS = [...ALL_QUERIES_METHODS, 'ByIcon']; + ruleTester.run(RULE_NAME, rule, { valid: [ - { - code: `getByText`, - }, - { - code: `const utils = render() - - utils.getByText + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}('Hello')`, + settings: { + 'testing-library/module': 'test-utils', + }, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `get${queryMethod}`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + const utils = render() + utils.get${queryMethod} `, - }, - { - code: `expect(getByText('foo')).toBeDefined()`, - }, - { - code: `const utils = render() - - expect(utils.getByText('foo')).toBeDefined() + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.get${queryMethod}`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + const utils = render() + expect(utils.get${queryMethod}('foo')).toBeDefined() + `, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(screen.get${queryMethod}('foo')).toBeDefined()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(getBy${queryMethod}('foo').bar).toBeInTheDocument()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + async () => { + await waitForElement(() => get${queryMethod}('foo')) + } + `, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `fireEvent.click(get${queryMethod}('bar'));`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = get${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `() => { return get${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `function bar() { return get${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const { get${queryMethod} } = render()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `it('test', () => { const { get${queryMethod} } = render() })`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `it('test', () => { const [ get${queryMethod} ] = render() })`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const a = [ get${queryMethod}('foo') ]`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const a = { foo: get${queryMethod}('bar') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `query${queryMethod}("foo")`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + expect(get${queryMethod}('foo')).toBeTruthy() + fireEvent.click(get${queryMethod}('bar')); `, - }, - { - code: `expect(getByText('foo')).toBeInTheDocument();`, - }, - { - code: `expect(getByText('foo').bar).toBeInTheDocument()`, - }, - { - code: `async () => { await waitForElement(() => getByText('foo')) }`, - }, - { - code: `fireEvent.click(getByText('bar'));`, - }, - { - code: `const quxElement = getByText('qux')`, - }, - { - code: `() => { return getByText('foo') }`, - }, - { - code: `function bar() { return getByText('foo') }`, - }, - { - code: `getByIcon('foo')`, // custom `getBy` query not extended through options - }, - { - code: `const { getByText } = render()`, - }, - { - code: `it('test', () => { const { getByText } = render() })`, - }, - { - code: `it('test', () => { const [ getByText ] = render() })`, - }, - { - code: `const a = [ getByText('foo') ]`, - }, - { - code: `const a = { foo: getByText('bar') }`, - }, - { - code: `queryByText("foo")`, - }, - { - code: `expect(getByText('foo')).toBeTruthy() - - fireEvent.click(getByText('bar'));`, options: [ { assertion: 'toBeTruthy', }, ], - }, - { - code: `expect(getByText('foo')).toBeEnabled()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeEnabled()`, options: [ { assertion: 'toBeInTheDocument', }, ], - }, + })), ], - invalid: [ - ...ALL_QUERIES_METHODS.map((queryMethod) => ({ + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ code: `get${queryMethod}('foo')`, errors: [ { @@ -95,63 +107,74 @@ ruleTester.run(RULE_NAME, rule, { }, ], })), - ...ALL_QUERIES_METHODS.map((queryMethod) => ({ - code: `const utils = render() - - utils.get${queryMethod}('foo')`, + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + const utils = render() + utils.get${queryMethod}('foo') + `, errors: [ { messageId: 'preferExplicitAssert', line: 3, - column: 13, + column: 15, }, ], })), - ...ALL_QUERIES_METHODS.map((queryMethod) => ({ - code: `() => { - get${queryMethod}('foo') - doSomething() + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `screen.get${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + line: 1, + column: 8, + }, + ], + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + () => { + get${queryMethod}('foo') + doSomething() - get${queryMethod}('bar') - const quxElement = get${queryMethod}('qux') - } + get${queryMethod}('bar') + const quxElement = get${queryMethod}('qux') + } `, errors: [ { messageId: 'preferExplicitAssert', - line: 2, + line: 3, }, { messageId: 'preferExplicitAssert', - line: 5, + line: 6, }, ], })), - // for coverage - { - code: `getByText("foo")`, - options: [{ customQueryNames: ['bar'] }], + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import "test-utils" + getBy${queryMethod}("Hello") + `, errors: [ { messageId: 'preferExplicitAssert', }, ], - }, + })), { code: `getByIcon('foo')`, // custom `getBy` query extended through options - options: [ - { - customQueryNames: ['getByIcon'], - }, - ], errors: [ { messageId: 'preferExplicitAssert', }, ], }, - { - code: `expect(getByText('foo')).toBeDefined()`, + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).toBeDefined()`, options: [ { assertion: 'toBeInTheDocument', @@ -160,13 +183,12 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssertAssertion', - column: 26, data: { assertion: 'toBeInTheDocument' }, }, ], - }, - { - code: `expect(getByText('foo')).not.toBeNull()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).not.toBeNull()`, options: [ { assertion: 'toBeInTheDocument', @@ -175,13 +197,12 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssertAssertion', - column: 26, data: { assertion: 'toBeInTheDocument' }, }, ], - }, - { - code: `expect(getByText('foo')).not.toBeFalsy()`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `expect(get${queryMethod}('foo')).not.toBeFalsy()`, options: [ { assertion: 'toBeInTheDocument', @@ -190,10 +211,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssertAssertion', - column: 26, data: { assertion: 'toBeInTheDocument' }, }, ], - }, + })), ], });