From 747fdb48317dd52c558176b076146e83eb19e287 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Sun, 8 Aug 2021 23:37:17 +0300 Subject: [PATCH 1/9] feat: report presence matchers --- lib/rules/prefer-find-by.ts | 200 +++++++++- tests/lib/rules/prefer-find-by.test.ts | 482 ++++++++++++++++++++----- 2 files changed, 576 insertions(+), 106 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 9699d4f3..4ac44480 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -111,7 +111,99 @@ export default createTestingLibraryRule({ }); } + // eslint-disable-next-line complexity + function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) { + // refactor to take node.body.callee + if (!isCallExpression(node.body)) { + return null; + } + + if ( + ASTUtils.isIdentifier(node.body.callee) && + helpers.isSyncQuery(node.body.callee) + ) { + return node.body.callee.name; + } + + if (!isMemberExpression(node.body.callee)) { + return null; + } + + if ( + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) + ) { + return node.body.callee.object.arguments[0].callee.name; + } + + if (!ASTUtils.isIdentifier(node.body.callee.property)) { + return null; + } + + if ( + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + isMemberExpression(node.body.callee.object.arguments[0].callee) && + ASTUtils.isIdentifier( + node.body.callee.object.arguments[0].callee.property + ) + ) { + return node.body.callee.object.arguments[0].callee.property.name; + } + + if ( + // expect().not + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + isMemberExpression( + node.body.callee.object.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee.property + ) + ) { + return node.body.callee.object.object.arguments[0].callee.property.name; + } + + if ( + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee + ) + ) { + return node.body.callee.object.object.arguments[0].callee.name; + } + + return node.body.callee.property.name; + } + + function getQueryArguments(node: TSESTree.CallExpression) { + if ( + isMemberExpression(node.callee) && + isCallExpression(node.callee.object) && + isCallExpression(node.callee.object.arguments[0]) + ) { + return node.callee.object.arguments[0].arguments; + } + + if ( + isMemberExpression(node.callee) && + isMemberExpression(node.callee.object) && + isCallExpression(node.callee.object.object) && + isCallExpression(node.callee.object.object.arguments[0]) + ) { + return node.callee.object.object.arguments[0].arguments; + } + + return node.arguments; + } + return { + // eslint-disable-next-line complexity 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { if ( !ASTUtils.isIdentifier(node.callee) || @@ -135,14 +227,74 @@ export default createTestingLibraryRule({ if ( isMemberExpression(argument.body.callee) && ASTUtils.isIdentifier(argument.body.callee.property) && - ASTUtils.isIdentifier(argument.body.callee.object) && - helpers.isSyncQuery(argument.body.callee.property) + (ASTUtils.isIdentifier(argument.body.callee.object) || + isCallExpression(argument.body.callee.object) || + isMemberExpression(argument.body.callee.object)) && + (helpers.isSyncQuery(argument.body.callee.property) || + (helpers.isPresenceAssert(argument.body.callee) && + isCallExpression(argument.body.callee.object) && + isCallExpression(argument.body.callee.object.arguments[0]) && + isMemberExpression( + argument.body.callee.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + argument.body.callee.object.arguments[0].callee.object + )) || + (isMemberExpression(argument.body.callee.object) && + helpers.isPresenceAssert(argument.body.callee.object) && + isCallExpression(argument.body.callee.object.object) && + isCallExpression( + argument.body.callee.object.object.arguments[0] + ) && + isMemberExpression( + argument.body.callee.object.object.arguments[0].callee + ))) ) { + let caller = ''; + + if (ASTUtils.isIdentifier(argument.body.callee.object)) { + // () => screen.getByText + caller = argument.body.callee.object.name; + } else if ( + // expect() + isCallExpression(argument.body.callee.object) && + ASTUtils.isIdentifier(argument.body.callee.object.callee) && + isCallExpression(argument.body.callee.object.arguments[0]) && + isMemberExpression( + argument.body.callee.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + argument.body.callee.object.arguments[0].callee.object + ) + ) { + caller = + argument.body.callee.object.arguments[0].callee.object.name; + } else if ( + // expect().not + isMemberExpression(argument.body.callee.object) && + isCallExpression(argument.body.callee.object.object) && + isCallExpression(argument.body.callee.object.object.arguments[0]) && + isMemberExpression( + argument.body.callee.object.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + argument.body.callee.object.object.arguments[0].callee.object + ) + ) { + caller = + argument.body.callee.object.object.arguments[0].callee.object + .name; + } + // shape of () => screen.getByText - const fullQueryMethod = argument.body.callee.property.name; - const caller = argument.body.callee.object.name; + const fullQueryMethod = getWrongQueryName(argument); + + if (!fullQueryMethod) { + return; + } + const queryVariant = getFindByQueryVariant(fullQueryMethod); - const callArguments = argument.body.arguments; + const callArguments = getQueryArguments(argument.body); const queryMethod = fullQueryMethod.split('By')[1]; reportInvalidUsage(node, { @@ -166,17 +318,47 @@ export default createTestingLibraryRule({ }); return; } + if ( - !ASTUtils.isIdentifier(argument.body.callee) || - !helpers.isSyncQuery(argument.body.callee) + (!ASTUtils.isIdentifier(argument.body.callee) || // () => getByText + !helpers.isSyncQuery(argument.body.callee)) && + (!isMemberExpression(argument.body.callee) || // wrapped in presence expect() + !isCallExpression(argument.body.callee.object) || + !isCallExpression(argument.body.callee.object.arguments[0]) || + !ASTUtils.isIdentifier( + argument.body.callee.object.arguments[0].callee + ) || + !helpers.isSyncQuery( + argument.body.callee.object.arguments[0].callee + ) || + !helpers.isPresenceAssert(argument.body.callee)) && + (!isMemberExpression(argument.body.callee) || // wrpaped in presence expect().not + !isMemberExpression(argument.body.callee.object) || + !isCallExpression(argument.body.callee.object.object) || + !isCallExpression( + argument.body.callee.object.object.arguments[0] + ) || + !ASTUtils.isIdentifier( + argument.body.callee.object.object.arguments[0].callee + ) || + !helpers.isSyncQuery( + argument.body.callee.object.object.arguments[0].callee + ) || + !helpers.isPresenceAssert(argument.body.callee.object)) ) { return; } + // shape of () => getByText - const fullQueryMethod = argument.body.callee.name; + const fullQueryMethod = getWrongQueryName(argument); + + if (!fullQueryMethod) { + return; + } + const queryMethod = fullQueryMethod.split('By')[1]; const queryVariant = getFindByQueryVariant(fullQueryMethod); - const callArguments = argument.body.arguments; + const callArguments = getQueryArguments(argument.body); reportInvalidUsage(node, { queryMethod, diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index e59c9b91..a8705141 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -63,7 +63,6 @@ ruleTester.run(RULE_NAME, rule, { ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` import {waitFor} from '@testing-library/foo'; - it('tests', async () => { await waitFor(function() { return ${queryMethod}('baz', { name: 'foo' }) @@ -74,7 +73,6 @@ ruleTester.run(RULE_NAME, rule, { { code: ` import {waitFor} from '@testing-library/foo'; - it('tests', async () => { await waitFor(() => myCustomFunction()) }) @@ -120,7 +118,16 @@ ruleTester.run(RULE_NAME, rule, { code: ` import {waitFor} from '@testing-library/foo'; it('tests', async () => { - await waitFor(() => expect(${queryMethod}('baz')).toBeInTheDocument()); + await waitFor(() => expect(screen.${queryMethod}('baz')).not.toBeInTheDocument()); + }) + `, + })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {waitFor} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + await waitFor(() => expect(${queryMethod}('baz')).not.toBeInTheDocument()); }) `, })), @@ -137,40 +144,11 @@ ruleTester.run(RULE_NAME, rule, { invalid: [ ...createScenario((waitMethod: string, queryMethod: string) => ({ code: ` - import {${waitMethod}} from '@testing-library/foo'; - it('tests', async () => { - const { ${queryMethod} } = render() - const submitButton = await ${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' })) - }) - `, - errors: [ - { - messageId: 'preferFindBy', - data: { - queryVariant: getFindByQueryVariant(queryMethod), - queryMethod: queryMethod.split('By')[1], - prevQuery: queryMethod, - waitForMethodName: waitMethod, - }, - }, - ], - output: ` - import {${waitMethod}} from '@testing-library/foo'; - it('tests', async () => { - const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() - const submitButton = await ${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) - }) - `, - })), - ...createScenario((waitMethod: string, queryMethod: string) => ({ - code: ` - import {${waitMethod}, screen} from '@testing-library/foo'; - it('tests', async () => { - const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' })) - }) - `, + import {${waitMethod}, screen} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo', { name: 'baz' })) + }) + `, errors: [ { messageId: 'preferFindBy', @@ -183,25 +161,25 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - import {${waitMethod}, screen} from '@testing-library/foo'; - it('tests', async () => { - const submitButton = await screen.${buildFindByMethod( - queryMethod - )}('foo', { name: 'baz' }) - }) - `, + import {${waitMethod}, screen} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, })), // // this scenario verifies it works when the render function is defined in another scope ...WAIT_METHODS.map( (waitMethod: string) => ({ code: ` - import {${waitMethod}} from '@testing-library/foo'; - const { getByText, queryByLabelText, findAllByRole } = customRender() - it('tests', async () => { - const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) - }) - `, + import {${waitMethod}} from '@testing-library/foo'; + const { getByText, queryByLabelText, findAllByRole } = customRender() + it('tests', async () => { + const submitButton = await ${waitMethod}(() => getByText('baz', { name: 'button' })) + }) + `, errors: [ { messageId: 'preferFindBy', @@ -214,12 +192,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - import {${waitMethod}} from '@testing-library/foo'; - const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() - it('tests', async () => { - const submitButton = await findByText('baz', { name: 'button' }) - }) - `, + import {${waitMethod}} from '@testing-library/foo'; + const { getByText, queryByLabelText, findAllByRole, findByText } = customRender() + it('tests', async () => { + const submitButton = await findByText('baz', { name: 'button' }) + }) + `, } as const) ), // // this scenario verifies when findBy* were already defined (because it was used elsewhere) @@ -227,12 +205,12 @@ ruleTester.run(RULE_NAME, rule, { (waitMethod: string) => ({ code: ` - import {${waitMethod}} from '@testing-library/foo'; - const { getAllByRole, findAllByRole } = customRender() - it('tests', async () => { - const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) - }) - `, + import {${waitMethod}} from '@testing-library/foo'; + const { getAllByRole, findAllByRole } = customRender() + it('tests', async () => { + const submitButton = await ${waitMethod}(() => getAllByRole('baz', { name: 'button' })) + }) + `, errors: [ { messageId: 'preferFindBy', @@ -245,12 +223,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - import {${waitMethod}} from '@testing-library/foo'; - const { getAllByRole, findAllByRole } = customRender() - it('tests', async () => { - const submitButton = await findAllByRole('baz', { name: 'button' }) - }) - `, + import {${waitMethod}} from '@testing-library/foo'; + const { getAllByRole, findAllByRole } = customRender() + it('tests', async () => { + const submitButton = await findAllByRole('baz', { name: 'button' }) + }) + `, } as const) ), // invalid code, as we need findBy* to be defined somewhere, but required for getting 100% coverage @@ -272,9 +250,9 @@ ruleTester.run(RULE_NAME, rule, { // this code would be invalid too, as findByRole is not defined anywhere. { code: ` - const getByRole = render().getByRole - const submitButton = await waitFor(() => getByRole('baz', { name: 'button' })) - `, + const getByRole = render().getByRole + const submitButton = await waitFor(() => getByRole('baz', { name: 'button' })) + `, errors: [ { messageId: 'preferFindBy', @@ -287,21 +265,21 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - const getByRole = render().getByRole - const submitButton = await findByRole('baz', { name: 'button' }) - `, + const getByRole = render().getByRole + const submitButton = await findByRole('baz', { name: 'button' }) + `, }, // custom query triggers the error but there is no fix - so output is the same ...WAIT_METHODS.map( (waitMethod: string) => ({ code: ` - import {${waitMethod},render} from '@testing-library/foo'; - it('tests', async () => { - const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) - }) - `, + import {${waitMethod},render} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + }) + `, errors: [ { messageId: 'preferFindBy', @@ -314,12 +292,12 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - import {${waitMethod},render} from '@testing-library/foo'; - it('tests', async () => { - const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) - }) - `, + import {${waitMethod},render} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => getByCustomQuery('baz')) + }) + `, } as const) ), // custom query triggers the error but there is no fix - so output is the same @@ -327,12 +305,12 @@ ruleTester.run(RULE_NAME, rule, { (waitMethod: string) => ({ code: ` - import {${waitMethod},render,screen} from '@testing-library/foo'; - it('tests', async () => { - const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) - }) - `, + import {${waitMethod},render,screen} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + }) + `, errors: [ { messageId: 'preferFindBy', @@ -345,13 +323,323 @@ ruleTester.run(RULE_NAME, rule, { }, ], output: ` - import {${waitMethod},render,screen} from '@testing-library/foo'; + import {${waitMethod},render,screen} from '@testing-library/foo'; + it('tests', async () => { + const { getByCustomQuery } = render() + const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + }) + `, + } as const) + ), + // presence matchers + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; it('tests', async () => { - const { getByCustomQuery } = render() - const submitButton = await ${waitMethod}(() => screen.getByCustomQuery('baz')) + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => ${queryMethod}('foo', { name: 'baz' })) }) `, - } as const) - ), + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).toBeInTheDocument()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).toBeDefined()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).not.toBeNull()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).toBeTruthy()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + const submitButton = await ${waitMethod}(() => expect(${queryMethod}('foo', { name: 'baz' })).not.toBeFalsy()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod}, ${buildFindByMethod(queryMethod)} } = render() + const submitButton = await ${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).toBeInTheDocument()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).toBeDefined()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).not.toBeNull()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).toBeTruthy()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), + ...createScenario((waitMethod: string, queryMethod: string) => ({ + code: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await ${waitMethod}(() => expect(screen.${queryMethod}('foo', { name: 'baz' })).not.toBeFalsy()) + }) + `, + errors: [ + { + messageId: 'preferFindBy', + data: { + queryVariant: getFindByQueryVariant(queryMethod), + queryMethod: queryMethod.split('By')[1], + prevQuery: queryMethod, + waitForMethodName: waitMethod, + }, + }, + ], + output: ` + import {${waitMethod}} from '@testing-library/foo'; + it('tests', async () => { + const submitButton = await screen.${buildFindByMethod( + queryMethod + )}('foo', { name: 'baz' }) + }) + `, + })), ], }); From 12d875d2fe5f5b21b8f58bdf6b7f159bde1e28c0 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Mon, 9 Aug 2021 23:26:16 +0300 Subject: [PATCH 2/9] refactor: split two functions --- lib/rules/prefer-find-by.ts | 117 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 48 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 4ac44480..64f0000d 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -111,24 +111,17 @@ export default createTestingLibraryRule({ }); } - // eslint-disable-next-line complexity - function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) { - // refactor to take node.body.callee - if (!isCallExpression(node.body)) { - return null; - } - + function getWrongQueryNameInAssertion( + node: TSESTree.ArrowFunctionExpression + ) { if ( - ASTUtils.isIdentifier(node.body.callee) && - helpers.isSyncQuery(node.body.callee) + !isCallExpression(node.body) || + !isMemberExpression(node.body.callee) ) { - return node.body.callee.name; - } - - if (!isMemberExpression(node.body.callee)) { return null; } + // expect(getByText).toBeInTheDocument() shape if ( isCallExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.arguments[0]) && @@ -141,6 +134,7 @@ export default createTestingLibraryRule({ return null; } + // expect(screen.getByText).toBeInTheDocument() shape if ( isCallExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.arguments[0]) && @@ -152,8 +146,8 @@ export default createTestingLibraryRule({ return node.body.callee.object.arguments[0].callee.property.name; } + // expect(screen.getByText).not shape if ( - // expect().not isMemberExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.object) && isCallExpression(node.body.callee.object.object.arguments[0]) && @@ -167,6 +161,7 @@ export default createTestingLibraryRule({ return node.body.callee.object.object.arguments[0].callee.property.name; } + // expect(getByText).not shape if ( isMemberExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.object) && @@ -181,6 +176,62 @@ export default createTestingLibraryRule({ return node.body.callee.property.name; } + function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) { + if (!isCallExpression(node.body)) { + return null; + } + + // expect(() => getByText) and expect(() => screen.getByText) shape + if ( + ASTUtils.isIdentifier(node.body.callee) && + helpers.isSyncQuery(node.body.callee) + ) { + return node.body.callee.name; + } + + return getWrongQueryNameInAssertion(node); + } + + function getCaller(node: TSESTree.ArrowFunctionExpression) { + if ( + !isCallExpression(node.body) || + !isMemberExpression(node.body.callee) + ) { + return null; + } + + if (ASTUtils.isIdentifier(node.body.callee.object)) { + // () => screen.getByText + return node.body.callee.object.name; + } else if ( + // expect() + isCallExpression(node.body.callee.object) && + ASTUtils.isIdentifier(node.body.callee.object.callee) && + isCallExpression(node.body.callee.object.arguments[0]) && + isMemberExpression(node.body.callee.object.arguments[0].callee) && + ASTUtils.isIdentifier( + node.body.callee.object.arguments[0].callee.object + ) + ) { + return node.body.callee.object.arguments[0].callee.object.name; + } else if ( + // expect().not + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + isMemberExpression( + node.body.callee.object.object.arguments[0].callee + ) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee.object + ) + ) { + return node.body.callee.object.object.arguments[0].callee.object.name; + } + + return null; + } + function getQueryArguments(node: TSESTree.CallExpression) { if ( isMemberExpression(node.callee) && @@ -250,40 +301,10 @@ export default createTestingLibraryRule({ argument.body.callee.object.object.arguments[0].callee ))) ) { - let caller = ''; - - if (ASTUtils.isIdentifier(argument.body.callee.object)) { - // () => screen.getByText - caller = argument.body.callee.object.name; - } else if ( - // expect() - isCallExpression(argument.body.callee.object) && - ASTUtils.isIdentifier(argument.body.callee.object.callee) && - isCallExpression(argument.body.callee.object.arguments[0]) && - isMemberExpression( - argument.body.callee.object.arguments[0].callee - ) && - ASTUtils.isIdentifier( - argument.body.callee.object.arguments[0].callee.object - ) - ) { - caller = - argument.body.callee.object.arguments[0].callee.object.name; - } else if ( - // expect().not - isMemberExpression(argument.body.callee.object) && - isCallExpression(argument.body.callee.object.object) && - isCallExpression(argument.body.callee.object.object.arguments[0]) && - isMemberExpression( - argument.body.callee.object.object.arguments[0].callee - ) && - ASTUtils.isIdentifier( - argument.body.callee.object.object.arguments[0].callee.object - ) - ) { - caller = - argument.body.callee.object.object.arguments[0].callee.object - .name; + const caller = getCaller(argument); + + if (!caller) { + return; } // shape of () => screen.getByText From 67f3d7718992edb146060ba9273a832cc8e39747 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Mon, 9 Aug 2021 23:34:34 +0300 Subject: [PATCH 3/9] refactor: split big if statement --- lib/rules/prefer-find-by.ts | 57 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 64f0000d..09e578a4 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -340,32 +340,39 @@ export default createTestingLibraryRule({ return; } + const isSyncQuery = + ASTUtils.isIdentifier(argument.body.callee) && // () => getByText + helpers.isSyncQuery(argument.body.callee); + + const isWrappedInPresenceAssert = + isMemberExpression(argument.body.callee) && + isCallExpression(argument.body.callee.object) && + isCallExpression(argument.body.callee.object.arguments[0]) && + ASTUtils.isIdentifier( + argument.body.callee.object.arguments[0].callee + ) && + helpers.isSyncQuery( + argument.body.callee.object.arguments[0].callee + ) && + helpers.isPresenceAssert(argument.body.callee); + + const isWrappedInNegatedPresenceAssert = + isMemberExpression(argument.body.callee) && // wrpaped in presence expect().not + isMemberExpression(argument.body.callee.object) && + isCallExpression(argument.body.callee.object.object) && + isCallExpression(argument.body.callee.object.object.arguments[0]) && + ASTUtils.isIdentifier( + argument.body.callee.object.object.arguments[0].callee + ) && + helpers.isSyncQuery( + argument.body.callee.object.object.arguments[0].callee + ) && + helpers.isPresenceAssert(argument.body.callee.object); + if ( - (!ASTUtils.isIdentifier(argument.body.callee) || // () => getByText - !helpers.isSyncQuery(argument.body.callee)) && - (!isMemberExpression(argument.body.callee) || // wrapped in presence expect() - !isCallExpression(argument.body.callee.object) || - !isCallExpression(argument.body.callee.object.arguments[0]) || - !ASTUtils.isIdentifier( - argument.body.callee.object.arguments[0].callee - ) || - !helpers.isSyncQuery( - argument.body.callee.object.arguments[0].callee - ) || - !helpers.isPresenceAssert(argument.body.callee)) && - (!isMemberExpression(argument.body.callee) || // wrpaped in presence expect().not - !isMemberExpression(argument.body.callee.object) || - !isCallExpression(argument.body.callee.object.object) || - !isCallExpression( - argument.body.callee.object.object.arguments[0] - ) || - !ASTUtils.isIdentifier( - argument.body.callee.object.object.arguments[0].callee - ) || - !helpers.isSyncQuery( - argument.body.callee.object.object.arguments[0].callee - ) || - !helpers.isPresenceAssert(argument.body.callee.object)) + !isSyncQuery && + !isWrappedInPresenceAssert && + !isWrappedInNegatedPresenceAssert ) { return; } From 54c4eae500b3683da01a5c4b1af052b60540e905 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Tue, 10 Aug 2021 00:00:32 +0300 Subject: [PATCH 4/9] refactor: split two functions --- lib/rules/prefer-find-by.ts | 145 ++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 65 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 09e578a4..bc0b50b8 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -232,6 +232,80 @@ export default createTestingLibraryRule({ return null; } + function isSyncQuery(node: TSESTree.ArrowFunctionExpression) { + if (!isCallExpression(node.body)) { + return false; + } + + const isQuery = + ASTUtils.isIdentifier(node.body.callee) && // () => getByText + helpers.isSyncQuery(node.body.callee); + + const isWrappedInPresenceAssert = + isMemberExpression(node.body.callee) && + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) && + helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) && + helpers.isPresenceAssert(node.body.callee); + + const isWrappedInNegatedPresenceAssert = + isMemberExpression(node.body.callee) && // wrpaped in presence expect().not + isMemberExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + ASTUtils.isIdentifier( + node.body.callee.object.object.arguments[0].callee + ) && + helpers.isSyncQuery( + node.body.callee.object.object.arguments[0].callee + ) && + helpers.isPresenceAssert(node.body.callee.object); + + return ( + isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert + ); + } + + function isScreenSyncQuery(node: TSESTree.ArrowFunctionExpression) { + if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) { + return false; + } + + if ( + !isMemberExpression(node.body.callee) || + !ASTUtils.isIdentifier(node.body.callee.property) + ) { + return false; + } + + if ( + !ASTUtils.isIdentifier(node.body.callee.object) && + !isCallExpression(node.body.callee.object) && + !isMemberExpression(node.body.callee.object) + ) { + return false; + } + + const yesA = + helpers.isPresenceAssert(node.body.callee) && + isCallExpression(node.body.callee.object) && + isCallExpression(node.body.callee.object.arguments[0]) && + isMemberExpression(node.body.callee.object.arguments[0].callee) && + ASTUtils.isIdentifier( + node.body.callee.object.arguments[0].callee.object + ); + + const yesB = + isMemberExpression(node.body.callee.object) && + helpers.isPresenceAssert(node.body.callee.object) && + isCallExpression(node.body.callee.object.object) && + isCallExpression(node.body.callee.object.object.arguments[0]) && + isMemberExpression(node.body.callee.object.object.arguments[0].callee); + + return helpers.isSyncQuery(node.body.callee.property) || yesA || yesB; + } + function getQueryArguments(node: TSESTree.CallExpression) { if ( isMemberExpression(node.callee) && @@ -254,7 +328,6 @@ export default createTestingLibraryRule({ } return { - // eslint-disable-next-line complexity 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { if ( !ASTUtils.isIdentifier(node.callee) || @@ -265,42 +338,17 @@ export default createTestingLibraryRule({ // ensure the only argument is an arrow function expression - if the arrow function is a block // we skip it const argument = node.arguments[0]; - if (!isArrowFunctionExpression(argument)) { - return; - } - if (!isCallExpression(argument.body)) { + if ( + !isArrowFunctionExpression(argument) || + !isCallExpression(argument.body) + ) { return; } const waitForMethodName = node.callee.name; // ensure here it's one of the sync methods that we are calling - if ( - isMemberExpression(argument.body.callee) && - ASTUtils.isIdentifier(argument.body.callee.property) && - (ASTUtils.isIdentifier(argument.body.callee.object) || - isCallExpression(argument.body.callee.object) || - isMemberExpression(argument.body.callee.object)) && - (helpers.isSyncQuery(argument.body.callee.property) || - (helpers.isPresenceAssert(argument.body.callee) && - isCallExpression(argument.body.callee.object) && - isCallExpression(argument.body.callee.object.arguments[0]) && - isMemberExpression( - argument.body.callee.object.arguments[0].callee - ) && - ASTUtils.isIdentifier( - argument.body.callee.object.arguments[0].callee.object - )) || - (isMemberExpression(argument.body.callee.object) && - helpers.isPresenceAssert(argument.body.callee.object) && - isCallExpression(argument.body.callee.object.object) && - isCallExpression( - argument.body.callee.object.object.arguments[0] - ) && - isMemberExpression( - argument.body.callee.object.object.arguments[0].callee - ))) - ) { + if (isScreenSyncQuery(argument)) { const caller = getCaller(argument); if (!caller) { @@ -340,40 +388,7 @@ export default createTestingLibraryRule({ return; } - const isSyncQuery = - ASTUtils.isIdentifier(argument.body.callee) && // () => getByText - helpers.isSyncQuery(argument.body.callee); - - const isWrappedInPresenceAssert = - isMemberExpression(argument.body.callee) && - isCallExpression(argument.body.callee.object) && - isCallExpression(argument.body.callee.object.arguments[0]) && - ASTUtils.isIdentifier( - argument.body.callee.object.arguments[0].callee - ) && - helpers.isSyncQuery( - argument.body.callee.object.arguments[0].callee - ) && - helpers.isPresenceAssert(argument.body.callee); - - const isWrappedInNegatedPresenceAssert = - isMemberExpression(argument.body.callee) && // wrpaped in presence expect().not - isMemberExpression(argument.body.callee.object) && - isCallExpression(argument.body.callee.object.object) && - isCallExpression(argument.body.callee.object.object.arguments[0]) && - ASTUtils.isIdentifier( - argument.body.callee.object.object.arguments[0].callee - ) && - helpers.isSyncQuery( - argument.body.callee.object.object.arguments[0].callee - ) && - helpers.isPresenceAssert(argument.body.callee.object); - - if ( - !isSyncQuery && - !isWrappedInPresenceAssert && - !isWrappedInNegatedPresenceAssert - ) { + if (!isSyncQuery(argument)) { return; } From 075347ce8bba5ef4db16f70a0e986d2836843313 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Tue, 10 Aug 2021 00:10:24 +0300 Subject: [PATCH 5/9] refactor: improve variable names --- lib/rules/prefer-find-by.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index bc0b50b8..0e3a54bb 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -287,7 +287,7 @@ export default createTestingLibraryRule({ return false; } - const yesA = + const isWrappedInPresenceAssert = helpers.isPresenceAssert(node.body.callee) && isCallExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.arguments[0]) && @@ -296,14 +296,18 @@ export default createTestingLibraryRule({ node.body.callee.object.arguments[0].callee.object ); - const yesB = + const isWrappedInNegatedPresenceAssert = isMemberExpression(node.body.callee.object) && helpers.isPresenceAssert(node.body.callee.object) && isCallExpression(node.body.callee.object.object) && isCallExpression(node.body.callee.object.object.arguments[0]) && isMemberExpression(node.body.callee.object.object.arguments[0].callee); - return helpers.isSyncQuery(node.body.callee.property) || yesA || yesB; + return ( + helpers.isSyncQuery(node.body.callee.property) || + isWrappedInPresenceAssert || + isWrappedInNegatedPresenceAssert + ); } function getQueryArguments(node: TSESTree.CallExpression) { From 398f742a3ae799127bf2a3c2c4515b1ab38849d8 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Tue, 10 Aug 2021 00:12:21 +0300 Subject: [PATCH 6/9] Add example in docs --- docs/rules/prefer-find-by.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/rules/prefer-find-by.md b/docs/rules/prefer-find-by.md index d85b296f..210e4c8c 100644 --- a/docs/rules/prefer-find-by.md +++ b/docs/rules/prefer-find-by.md @@ -26,6 +26,15 @@ const submitButton = await waitFor(() => const submitButton = await waitFor(() => queryAllByText('button', { name: /submit/i }) ); + +// arrow functions with one statement, calling any sync query method with presence assertion +const submitButton = await waitFor(() => + expect(queryByLabel('button', { name: /submit/i })).toBeInTheDocument() +); + +const submitButton = await waitFor(() => + expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy() +); ``` Examples of **correct** code for this rule: From 8086b78750059151a2ff8a5e89c06f81c679b1ee Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Tue, 10 Aug 2021 00:16:07 +0300 Subject: [PATCH 7/9] refactor: remove redundant comments --- lib/rules/prefer-find-by.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 0e3a54bb..96dbc386 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -238,7 +238,7 @@ export default createTestingLibraryRule({ } const isQuery = - ASTUtils.isIdentifier(node.body.callee) && // () => getByText + ASTUtils.isIdentifier(node.body.callee) && helpers.isSyncQuery(node.body.callee); const isWrappedInPresenceAssert = @@ -250,7 +250,7 @@ export default createTestingLibraryRule({ helpers.isPresenceAssert(node.body.callee); const isWrappedInNegatedPresenceAssert = - isMemberExpression(node.body.callee) && // wrpaped in presence expect().not + isMemberExpression(node.body.callee) && isMemberExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.object) && isCallExpression(node.body.callee.object.object.arguments[0]) && From fb6e352b79645f8f144833ae8d011ed51a796e22 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Sat, 21 Aug 2021 16:03:35 +0300 Subject: [PATCH 8/9] Refactor else if -> if --- lib/rules/prefer-find-by.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/rules/prefer-find-by.ts b/lib/rules/prefer-find-by.ts index 96dbc386..97c99300 100644 --- a/lib/rules/prefer-find-by.ts +++ b/lib/rules/prefer-find-by.ts @@ -203,7 +203,9 @@ export default createTestingLibraryRule({ if (ASTUtils.isIdentifier(node.body.callee.object)) { // () => screen.getByText return node.body.callee.object.name; - } else if ( + } + + if ( // expect() isCallExpression(node.body.callee.object) && ASTUtils.isIdentifier(node.body.callee.object.callee) && @@ -214,7 +216,9 @@ export default createTestingLibraryRule({ ) ) { return node.body.callee.object.arguments[0].callee.object.name; - } else if ( + } + + if ( // expect().not isMemberExpression(node.body.callee.object) && isCallExpression(node.body.callee.object.object) && From 2b64d92a0d83908ea771f0677ff1b8d17230f455 Mon Sep 17 00:00:00 2001 From: tozaicevas Date: Sat, 21 Aug 2021 16:17:48 +0300 Subject: [PATCH 9/9] test: add test case with an allowed assertion --- tests/lib/rules/prefer-find-by.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/lib/rules/prefer-find-by.test.ts b/tests/lib/rules/prefer-find-by.test.ts index a8705141..7975a023 100644 --- a/tests/lib/rules/prefer-find-by.test.ts +++ b/tests/lib/rules/prefer-find-by.test.ts @@ -114,6 +114,15 @@ ruleTester.run(RULE_NAME, rule, { }) `, })), + ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ + code: ` + import {screen, waitFor} from '@testing-library/foo'; + it('tests', async () => { + const { ${queryMethod} } = render() + await waitFor(() => expect(${queryMethod}('baz')).toBeDisabled()); + }) + `, + })), ...SYNC_QUERIES_COMBINATIONS.map((queryMethod) => ({ code: ` import {waitFor} from '@testing-library/foo';