diff --git a/lib/rules/prefer-screen-queries.ts b/lib/rules/prefer-screen-queries.ts index eb649f72..bfdd2773 100644 --- a/lib/rules/prefer-screen-queries.ts +++ b/lib/rules/prefer-screen-queries.ts @@ -2,6 +2,9 @@ import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { + getDeepestIdentifierNode, + getFunctionName, + getInnermostReturningFunction, isCallExpression, isMemberExpression, isObjectExpression, @@ -54,6 +57,22 @@ export default createTestingLibraryRule({ defaultOptions: [], create(context, _, helpers) { + const renderWrapperNames: string[] = []; + + function detectRenderWrapper(node: TSESTree.Identifier): void { + const innerFunction = getInnermostReturningFunction(context, node); + + if (innerFunction) { + renderWrapperNames.push(getFunctionName(innerFunction)); + } + } + + function isReportableRender(node: TSESTree.Identifier): boolean { + return ( + helpers.isRenderUtil(node) || renderWrapperNames.includes(node.name) + ); + } + function reportInvalidUsage(node: TSESTree.Identifier) { context.report({ node, @@ -78,6 +97,10 @@ export default createTestingLibraryRule({ } } + function isIdentifierAllowed(name: string) { + return ['screen', ...withinDeclaredVariables].includes(name); + } + // keep here those queries which are safe and shouldn't be reported // (from within, from render + container/base element, not related to TL, etc) const safeDestructuredQueries: string[] = []; @@ -93,7 +116,7 @@ export default createTestingLibraryRule({ return; } - const isComingFromValidRender = helpers.isRenderUtil(node.init.callee); + const isComingFromValidRender = isReportableRender(node.init.callee); if (!isComingFromValidRender) { // save the destructured query methods as safe since they are coming @@ -113,52 +136,54 @@ export default createTestingLibraryRule({ // save the destructured query methods as safe since they are coming // from within or render + base/container options saveSafeDestructuredQueries(node); - return; - } - - if (ASTUtils.isIdentifier(node.id)) { + } else if (ASTUtils.isIdentifier(node.id)) { withinDeclaredVariables.push(node.id.name); } }, - 'CallExpression > Identifier'(node: TSESTree.Identifier) { - if (!helpers.isBuiltInQuery(node)) { + CallExpression(node) { + const identifierNode = getDeepestIdentifierNode(node); + + if (!identifierNode) { return; } - if ( - !safeDestructuredQueries.some((queryName) => queryName === node.name) - ) { - reportInvalidUsage(node); + if (helpers.isRenderUtil(identifierNode)) { + detectRenderWrapper(identifierNode); } - }, - 'MemberExpression > Identifier'(node: TSESTree.Identifier) { - function isIdentifierAllowed(name: string) { - return ['screen', ...withinDeclaredVariables].includes(name); + + if (!helpers.isBuiltInQuery(identifierNode)) { + return; } - if (!helpers.isBuiltInQuery(node)) { + if (!isMemberExpression(identifierNode.parent)) { + const isSafeDestructuredQuery = safeDestructuredQueries.some( + (queryName) => queryName === identifierNode.name + ); + if (isSafeDestructuredQuery) { + return; + } + + reportInvalidUsage(identifierNode); return; } + const memberExpressionNode = identifierNode.parent; if ( - ASTUtils.isIdentifier(node) && - isMemberExpression(node.parent) && - isCallExpression(node.parent.object) && - ASTUtils.isIdentifier(node.parent.object.callee) && - node.parent.object.callee.name !== 'within' && - helpers.isRenderUtil(node.parent.object.callee) && - !usesContainerOrBaseElement(node.parent.object) + isCallExpression(memberExpressionNode.object) && + ASTUtils.isIdentifier(memberExpressionNode.object.callee) && + memberExpressionNode.object.callee.name !== 'within' && + isReportableRender(memberExpressionNode.object.callee) && + !usesContainerOrBaseElement(memberExpressionNode.object) ) { - reportInvalidUsage(node); + reportInvalidUsage(identifierNode); return; } if ( - isMemberExpression(node.parent) && - ASTUtils.isIdentifier(node.parent.object) && - !isIdentifierAllowed(node.parent.object.name) + ASTUtils.isIdentifier(memberExpressionNode.object) && + !isIdentifierAllowed(memberExpressionNode.object.name) ) { - reportInvalidUsage(node); + reportInvalidUsage(identifierNode); } }, }; diff --git a/tests/lib/rules/prefer-screen-queries.test.ts b/tests/lib/rules/prefer-screen-queries.test.ts index 052433b9..c94a1474 100644 --- a/tests/lib/rules/prefer-screen-queries.test.ts +++ b/tests/lib/rules/prefer-screen-queries.test.ts @@ -48,7 +48,7 @@ ruleTester.run(RULE_NAME, rule, { (query) => ` import { render } from '@testing-library/react' import { ${query} } from 'custom-queries' - + test("imported custom queries, since they can't be used through screen", () => { render(foo) ${query}('bar') @@ -58,7 +58,7 @@ ruleTester.run(RULE_NAME, rule, { ...CUSTOM_QUERY_COMBINATIONS.map( (query) => ` import { render } from '@testing-library/react' - + test("render-returned custom queries, since they can't be used through screen", () => { const { ${query} } = render(foo) ${query}('bar') @@ -71,7 +71,7 @@ ruleTester.run(RULE_NAME, rule, { }, code: ` import { render } from '@testing-library/react' - + test("custom queries + custom-queries setting, since they can't be used through screen", () => { const { ${query} } = render(foo) ${query}('bar') @@ -413,5 +413,80 @@ ruleTester.run(RULE_NAME, rule, { ], } as const) ), + { + code: ` // issue #367 - example A + import { render } from '@testing-library/react'; + + function setup() { + return render(
); + } + + it('foo', async () => { + const { getByText } = await setup(); + expect(getByText('foo')).toBeInTheDocument(); + }); + + it('bar', () => { + const { getByText } = setup(); + expect(getByText('foo')).toBeInTheDocument(); + }); + `, + errors: [ + { + messageId: 'preferScreenQueries', + line: 10, + column: 16, + data: { + name: 'getByText', + }, + }, + { + messageId: 'preferScreenQueries', + line: 15, + column: 16, + data: { + name: 'getByText', + }, + }, + ], + }, + { + code: ` // issue #367 - example B + import { render } from '@testing-library/react'; + + function setup() { + return render(
); + } + + it('foo', () => { + const { getByText } = setup(); + expect(getByText('foo')).toBeInTheDocument(); + }); + + it('bar', () => { + const results = setup(); + const { getByText } = results; + expect(getByText('foo')).toBe('foo'); + }); + `, + errors: [ + { + messageId: 'preferScreenQueries', + line: 10, + column: 16, + data: { + name: 'getByText', + }, + }, + { + messageId: 'preferScreenQueries', + line: 16, + column: 16, + data: { + name: 'getByText', + }, + }, + ], + }, ], });