diff --git a/README.md b/README.md index 7af9d248..14a2310d 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ To enable this configuration use the `extends` property in your | [`testing-library/no-wait-for-multiple-assertions`](./docs/rules/no-wait-for-multiple-assertions.md) | Disallow the use of multiple `expect` calls inside `waitFor` | | | | [`testing-library/no-wait-for-side-effects`](./docs/rules/no-wait-for-side-effects.md) | Disallow the use of side effects in `waitFor` | | | | [`testing-library/no-wait-for-snapshot`](./docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | | -| [`testing-library/prefer-explicit-assert`](./docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | +| [`testing-library/prefer-explicit-assert`](./docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | | | [`testing-library/prefer-find-by`](./docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | 🔧 | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | [`testing-library/prefer-presence-queries`](./docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | | | | [`testing-library/prefer-query-by-disappearance`](./docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | | | diff --git a/lib/rules/prefer-explicit-assert.ts b/lib/rules/prefer-explicit-assert.ts index 98c23b5d..20ffc076 100644 --- a/lib/rules/prefer-explicit-assert.ts +++ b/lib/rules/prefer-explicit-assert.ts @@ -1,7 +1,11 @@ import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; -import { findClosestCallNode, isMemberExpression } from '../node-utils'; +import { + findClosestCallNode, + isCallExpression, + isMemberExpression, +} from '../node-utils'; import { PRESENCE_MATCHERS, ABSENCE_MATCHERS } from '../utils'; export const RULE_NAME = 'prefer-explicit-assert'; @@ -15,7 +19,47 @@ type Options = [ ]; const isAtTopLevel = (node: TSESTree.Node) => - !!node.parent?.parent && node.parent.parent.type === 'ExpressionStatement'; + (!!node.parent?.parent && + node.parent.parent.type === 'ExpressionStatement') || + (node.parent?.parent?.type === 'AwaitExpression' && + !!node.parent.parent.parent && + node.parent.parent.parent.type === 'ExpressionStatement'); + +const isVariableDeclaration = (node: TSESTree.Node) => { + if ( + isCallExpression(node.parent) && + ASTUtils.isAwaitExpression(node.parent.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent.parent) + ) { + return true; // const quxElement = await findByLabelText('qux') + } + + if ( + isCallExpression(node.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent) + ) { + return true; // const quxElement = findByLabelText('qux') + } + + if ( + isMemberExpression(node.parent) && + isCallExpression(node.parent.parent) && + ASTUtils.isAwaitExpression(node.parent.parent.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent.parent.parent) + ) { + return true; // const quxElement = await screen.findByLabelText('qux') + } + + if ( + isMemberExpression(node.parent) && + isCallExpression(node.parent.parent) && + ASTUtils.isVariableDeclarator(node.parent.parent.parent) + ) { + return true; // const quxElement = screen.findByLabelText('qux') + } + + return false; +}; export default createTestingLibraryRule({ name: RULE_NAME, @@ -23,7 +67,7 @@ export default createTestingLibraryRule({ type: 'suggestion', docs: { description: - 'Suggest using explicit assertions rather than just `getBy*` queries', + 'Suggest using explicit assertions rather than standalone queries', category: 'Best Practices', recommendedConfig: { dom: false, @@ -34,9 +78,9 @@ export default createTestingLibraryRule({ }, messages: { preferExplicitAssert: - 'Wrap stand-alone `getBy*` query with `expect` function for better explicit assertion', + 'Wrap stand-alone `{{queryType}}` query with `expect` function for better explicit assertion', preferExplicitAssertAssertion: - '`getBy*` queries must be asserted with `{{assertion}}`', + '`getBy*` queries must be asserted with `{{assertion}}`', // TODO: support findBy* queries as well }, schema: [ { @@ -55,14 +99,40 @@ export default createTestingLibraryRule({ create(context, [options], helpers) { const { assertion } = options; const getQueryCalls: TSESTree.Identifier[] = []; + const findQueryCalls: TSESTree.Identifier[] = []; return { 'CallExpression Identifier'(node: TSESTree.Identifier) { if (helpers.isGetQueryVariant(node)) { getQueryCalls.push(node); } + + if (helpers.isFindQueryVariant(node)) { + findQueryCalls.push(node); + } }, 'Program:exit'() { + findQueryCalls.forEach((queryCall) => { + const memberExpression = isMemberExpression(queryCall.parent) + ? queryCall.parent + : queryCall; + + if ( + isVariableDeclaration(queryCall) || + !isAtTopLevel(memberExpression) + ) { + return; + } + + context.report({ + node: queryCall, + messageId: 'preferExplicitAssert', + data: { + queryType: 'findBy*', + }, + }); + }); + getQueryCalls.forEach((queryCall) => { const node = isMemberExpression(queryCall.parent) ? queryCall.parent @@ -72,6 +142,9 @@ export default createTestingLibraryRule({ context.report({ node: queryCall, messageId: 'preferExplicitAssert', + data: { + queryType: 'getBy*', + }, }); } diff --git a/tests/lib/rules/prefer-explicit-assert.test.ts b/tests/lib/rules/prefer-explicit-assert.test.ts index 03c60349..2f40a380 100644 --- a/tests/lib/rules/prefer-explicit-assert.test.ts +++ b/tests/lib/rules/prefer-explicit-assert.test.ts @@ -54,6 +54,52 @@ ruleTester.run(RULE_NAME, rule, { ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ code: `const quxElement = get${queryMethod}('qux')`, })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + async () => { + const quxElement = await find${queryMethod}('qux') + }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = find${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: `const quxElement = screen.find${queryMethod}('qux')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + async () => { + const quxElement = await screen.find${queryMethod}('qux') + }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + function findBySubmit() { + return screen.find${queryMethod}('foo') + }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + function findBySubmit() { + return find${queryMethod}('foo') + }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + () => { return screen.find${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + () => { return find${queryMethod}('foo') }`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + () => screen.find${queryMethod}('foo')`, + })), + ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ + code: ` + () => find${queryMethod}('foo')`, + })), ...COMBINED_QUERIES_METHODS.map((queryMethod) => ({ code: `() => { return get${queryMethod}('foo') }`, })), @@ -106,6 +152,65 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssert', + data: { + queryType: 'getBy*', + }, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `find${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: `screen.find${queryMethod}('foo')`, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + async () => { + await screen.find${queryMethod}('foo') + } + `, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, + }, + ], + } as const) + ), + ...COMBINED_QUERIES_METHODS.map( + (queryMethod) => + ({ + code: ` + async () => { + await find${queryMethod}('foo') + } + `, + errors: [ + { + messageId: 'preferExplicitAssert', + data: { queryType: 'findBy*' }, }, ], } as const) @@ -122,6 +227,9 @@ ruleTester.run(RULE_NAME, rule, { messageId: 'preferExplicitAssert', line: 3, column: 15, + data: { + queryType: 'getBy*', + }, }, ], } as const) @@ -135,6 +243,9 @@ ruleTester.run(RULE_NAME, rule, { messageId: 'preferExplicitAssert', line: 1, column: 8, + data: { + queryType: 'getBy*', + }, }, ], } as const) @@ -155,10 +266,16 @@ ruleTester.run(RULE_NAME, rule, { { messageId: 'preferExplicitAssert', line: 3, + data: { + queryType: 'getBy*', + }, }, { messageId: 'preferExplicitAssert', line: 6, + data: { + queryType: 'getBy*', + }, }, ], } as const) @@ -176,6 +293,9 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: 'preferExplicitAssert', + data: { + queryType: 'getBy*', + }, }, ], } as const)