Skip to content

Commit

Permalink
feat: add prefer-find-by rule
Browse files Browse the repository at this point in the history
  • Loading branch information
gndelia committed May 22, 2020
1 parent b3e46d8 commit 3d9d0d8
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 0 deletions.
43 changes: 43 additions & 0 deletions docs/rules/prefer-find-by.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Use `find*` query methods to wait for elements instead of waitFor (prefer-find-by)

TBD

## Rule details

This rule aims to use `findBy*` or `findAllBy*` queries to wait for elements, rather than using `waitFor`, or the deprecated methods `waitForElement` and `wait`

Examples of **incorrect** code for this rule

```js
const submitButton = await waitFor(() =>
screen.getByRole('button', { name: /submit/i })
);

const submitButton = await waitFor(() =>
screen.getAllTestId('button', { name: /submit/i })
);

const submitButton = await waitFor(() =>
queryByLabel('button', { name: /submit/i })
);

const submitButton = await waitFor(() =>
queryAllByText('button', { name: /submit/i })
);
```

Examples of **correct** code for this rule:

```js
const submitButton = await findByText('foo');

const submitButton = await screen.findAllByRole('table');
```

## When Not To Use It

TBD

## Further Reading

TBD
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import preferExplicitAssert from './rules/prefer-explicit-assert';
import preferPresenceQueries from './rules/prefer-presence-queries';
import preferScreenQueries from './rules/prefer-screen-queries';
import preferWaitFor from './rules/prefer-wait-for';
import preferFindBy from './rules/prefer-find-by';

const rules = {
'await-async-query': awaitAsyncQuery,
Expand All @@ -23,6 +24,7 @@ const rules = {
'no-manual-cleanup': noManualCleanup,
'no-wait-for-empty-callback': noWaitForEmptyCallback,
'prefer-explicit-assert': preferExplicitAssert,
'prefer-find-by': preferFindBy,
'prefer-presence-queries': preferPresenceQueries,
'prefer-screen-queries': preferScreenQueries,
'prefer-wait-for': preferWaitFor,
Expand Down
4 changes: 4 additions & 0 deletions lib/node-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,7 @@ export function hasThenProperty(node: TSESTree.Node) {
node.property.name === 'then'
);
}

export function isArrowFunctionExpression(node: TSESTree.Node): node is TSESTree.ArrowFunctionExpression {
return node.type === 'ArrowFunctionExpression'
}
88 changes: 88 additions & 0 deletions lib/rules/prefer-find-by.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
import {
isIdentifier,
isCallExpression,
isMemberExpression,
isArrowFunctionExpression,
} from '../node-utils';
import { getDocsUrl, SYNC_QUERIES_COMBINATIONS } from '../utils';

export const RULE_NAME = 'prefer-find-by';

type Options = [];
export type MessageIds = 'preferFindBy';
// TODO check if this should be under utils.ts - there are some async utils
export const WAIT_METHODS = ['waitFor', 'waitForElement', 'wait']

export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using find* instead of waitFor to wait for elements',
category: 'Best Practices',
recommended: false,
},
messages: {
preferFindBy: 'Prefer {{queryVariant}}{{queryMethod}} method over using await {{fullQuery}}'
},
fixable: null,
schema: []
},
defaultOptions: [],

create(context) {

function reportInvalidUsage(node: TSESTree.CallExpression, { queryVariant, queryMethod, fullQuery }: { queryVariant: string, queryMethod: string, fullQuery: string}) {
context.report({
node,
messageId: "preferFindBy",
data: { queryVariant, queryMethod, fullQuery },
});
}

const sourceCode = context.getSourceCode();

return {
'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) {
if (!isIdentifier(node.callee) || !WAIT_METHODS.includes(node.callee.name)) {
return
}
// 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)) {
return
}
// ensure here it's one of the sync methods that we are calling
if (isMemberExpression(argument.body.callee) && isIdentifier(argument.body.callee.property) && isIdentifier(argument.body.callee.object) && SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.property.name)) {
// shape of () => screen.getByText
const queryMethod = argument.body.callee.property.name
reportInvalidUsage(node, {
queryMethod: queryMethod.split('By')[1],
queryVariant: getFindByQueryVariant(queryMethod),
fullQuery: sourceCode.getText(node)
})
return
}
if (isIdentifier(argument.body.callee) && SYNC_QUERIES_COMBINATIONS.includes(argument.body.callee.name)) {
// shape of () => getByText
const queryMethod = argument.body.callee.name
reportInvalidUsage(node, {
queryMethod: queryMethod.split('By')[1],
queryVariant: getFindByQueryVariant(queryMethod),
fullQuery: sourceCode.getText(node)
})
return
}
}
}
}
})

function getFindByQueryVariant(queryMethod: string) {
return queryMethod.includes('All') ? 'findAllBy' : 'findBy'
}
55 changes: 55 additions & 0 deletions tests/lib/rules/prefer-find-by.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { InvalidTestCase } from '@typescript-eslint/experimental-utils/dist/ts-eslint'
import { createRuleTester } from '../test-utils';
import { ASYNC_QUERIES_COMBINATIONS, SYNC_QUERIES_COMBINATIONS } from '../../../lib/utils';
import rule, { WAIT_METHODS, RULE_NAME } from '../../../lib/rules/prefer-find-by';

const ruleTester = createRuleTester({
ecmaFeatures: {
jsx: true,
},
});

ruleTester.run(RULE_NAME, rule, {
valid: [
...ASYNC_QUERIES_COMBINATIONS.map((queryMethod: string) => ({
code: `const submitButton = await ${queryMethod}('foo')`
})),
...ASYNC_QUERIES_COMBINATIONS.map((queryMethod: string) => ({
code: `const submitButton = await screen.${queryMethod}('foo')`
}))
],
invalid: [
// using reduce + concat 'cause flatMap is not available in node10.x
...WAIT_METHODS.reduce((acc: InvalidTestCase<'preferFindBy', []>[], waitMethod) => acc
.concat(
SYNC_QUERIES_COMBINATIONS.map((queryMethod: string) => ({
code: `
const submitButton = await ${waitMethod}(() => ${queryMethod}('foo'))
`,
errors: [{
messageId: 'preferFindBy',
data: {
queryVariant: queryMethod.includes('All') ? 'findAllBy': 'findBy',
queryMethod: queryMethod.split('By')[1],
fullQuery: `${waitMethod}(() => ${queryMethod}('foo'))`,
}
}]
}))
).concat(
SYNC_QUERIES_COMBINATIONS.map((queryMethod: string) => ({
code: `
const submitButton = await ${waitMethod}(() => screen.${queryMethod}('foo'))
`,
errors: [{
messageId: 'preferFindBy',
data: {
queryVariant: queryMethod.includes('All') ? 'findAllBy': 'findBy',
queryMethod: queryMethod.split('By')[1],
fullQuery: `${waitMethod}(() => screen.${queryMethod}('foo'))`,
}
}]
}))
),
[])
],
})

0 comments on commit 3d9d0d8

Please sign in to comment.