-
Notifications
You must be signed in to change notification settings - Fork 150
feat(rule): prefer expect queryBy #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
62ed216
353cc77
093b44b
e3e4231
53a950c
0fd4a6c
6a3f264
67d305c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# Disallow the use of `expect(getBy*)` (prefer-expect-query-by) | ||
|
||
The (DOM) Testing Library support three types of queries: `getBy*` and `queryBy*`. Using `getBy*` throws an error in case the element is not found. This is useful when using method like `waitForElement`, which are `async` functions that will wait for the element to be found until a certain timeout, after that the test will fail. | ||
However, when trying to assert if an element is not in the document, we can't use `getBy*` as the test will fail immediately. Instead it is recommended to use `queryBy*`, which does not throw and therefore we can assert that e.g. `expect(queryByText("Foo")).not.toBeInTheDocument()`. | ||
|
||
> The same applies for the `getAll*` and `queryAll*` queries. | ||
|
||
## Rule details | ||
|
||
This rule gives a notification whenever `expect` is used with one of the query functions that throw an error if the element is not found. | ||
|
||
This rule is enabled by default. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
test('some test', () => { | ||
const { getByText, getAllByText } = render(<App />); | ||
expect(getByText('Foo')).toBeInTheDocument(); | ||
expect(getAllByText('Foo')[0]).toBeInTheDocument(); | ||
expect(getByText('Foo')).not.toBeInTheDocument(); | ||
expect(getAllByText('Foo')[0]).not.toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
```js | ||
test('some test', () => { | ||
const rendered = render(<App />); | ||
expect(rendered.getByText('Foo')).toBeInTheDocument(); | ||
expect(rendered.getAllByText('Foo')[0]).toBeInTheDocument(); | ||
expect(rendered.getByText('Foo')).not.toBeInTheDocument(); | ||
expect(rendered.getAllByText('Foo')[0]).not.toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```js | ||
test('some test', () => { | ||
const { queryByText, queryAllByText } = render(<App />); | ||
expect(queryByText('Foo')).toBeInTheDocument(); | ||
expect(queryAllByText('Foo')[0]).toBeInTheDocument(); | ||
expect(queryByText('Foo')).not.toBeInTheDocument(); | ||
expect(queryAllByText('Foo')[0]).not.toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
```js | ||
test('some test', () => { | ||
const rendered = render(<App />); | ||
expect(rendered.queryByText('Foo')).toBeInTheDocument(); | ||
expect(rendered.queryAllByText('Foo')[0]).toBeInTheDocument(); | ||
expect(rendered.queryByText('Foo')).not.toBeInTheDocument(); | ||
expect(rendered.queryAllByText('Foo')[0]).not.toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
## Further Reading | ||
|
||
- [Appearance and Disappearance](https://testing-library.com/docs/guide-disappearance#asserting-elements-are-not-present) | ||
- [Testing Library queries cheatsheet](https://testing-library.com/docs/dom-testing-library/cheatsheet#queries) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
'use strict'; | ||
|
||
const { getDocsUrl } = require('../utils'); | ||
|
||
const AST_NODE_TYPES = { | ||
Belco90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Identifier: 'Identifier', | ||
MemberExpression: 'MemberExpression', | ||
}; | ||
|
||
function isIdentifier(node) { | ||
return node.type === AST_NODE_TYPES.Identifier; | ||
} | ||
|
||
function isMemberExpression(node) { | ||
return node.type === AST_NODE_TYPES.MemberExpression; | ||
} | ||
|
||
function isUsingWrongQueries(node) { | ||
return node.name.startsWith('getBy') || node.name.startsWith('getAllBy'); | ||
} | ||
|
||
function isNotNullOrUndefined(input) { | ||
return input != null; | ||
} | ||
|
||
function mapNodesForWrongGetByQuery(node) { | ||
const nodeArguments = node.arguments; | ||
return nodeArguments | ||
.map(arg => { | ||
if (!arg.callee) { | ||
return null; | ||
} | ||
// Example: `expect(rendered.getBy*)` | ||
Belco90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (isMemberExpression(arg.callee)) { | ||
const node = arg.callee.property; | ||
if (isIdentifier(node) && isUsingWrongQueries(node)) { | ||
return node; | ||
} | ||
return null; | ||
} | ||
|
||
// Example: `expect(getBy*)` | ||
if (isIdentifier(arg.callee) && isUsingWrongQueries(arg.callee)) { | ||
return arg.callee; | ||
} | ||
|
||
return null; | ||
}) | ||
.filter(isNotNullOrUndefined); | ||
} | ||
|
||
function hasExpectWithWrongGetByQuery(node) { | ||
if ( | ||
node.callee && | ||
node.callee.type === AST_NODE_TYPES.Identifier && | ||
node.callee.name === 'expect' && | ||
node.arguments | ||
) { | ||
const nodesGetBy = mapNodesForWrongGetByQuery(node); | ||
return nodesGetBy.length > 0; | ||
} | ||
return false; | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
category: 'Best Practices', | ||
description: 'Disallow using getBy* queries in expect calls', | ||
recommended: 'error', | ||
url: getDocsUrl('prefer-expect-query-by'), | ||
}, | ||
messages: { | ||
expectQueryBy: | ||
'Using `expect(getBy*)` is not recommended, use `expect(queryBy*)` instead.', | ||
}, | ||
schema: [], | ||
type: 'suggestion', | ||
fixable: null, | ||
}, | ||
|
||
create: context => ({ | ||
CallExpression(node) { | ||
if (hasExpectWithWrongGetByQuery(node)) { | ||
// const nodesGetBy = mapNodesForWrongGetByQuery(node); | ||
context.report({ | ||
node: node.callee, | ||
messageId: 'expectQueryBy', | ||
// TODO: we keep the autofixing disabled for now, until we figure out | ||
// a better way to amend for the edge cases. | ||
// See also the related discussion: https://github.com/Belco90/eslint-plugin-testing-library/pull/22#discussion_r335394402 | ||
// fix(fixer) { | ||
// return fixer.replaceText( | ||
// nodesGetBy[0], | ||
// nodesGetBy[0].name.replace(/^(get(All)?(.*))$/, 'query$2$3') | ||
// ); | ||
// }, | ||
}); | ||
} | ||
}, | ||
}), | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
'use strict'; | ||
|
||
const RuleTester = require('eslint').RuleTester; | ||
const rule = require('../../../lib/rules/prefer-expect-query-by'); | ||
const { ALL_QUERIES_METHODS } = require('../../../lib/utils'); | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, | ||
}); | ||
|
||
const queryByVariants = ALL_QUERIES_METHODS.reduce( | ||
(variants, method) => [ | ||
...variants, | ||
...[`query${method}`, `queryAll${method}`], | ||
], | ||
[] | ||
); | ||
const getByVariants = ALL_QUERIES_METHODS.reduce( | ||
(variants, method) => [...variants, ...[`get${method}`, `getAll${method}`]], | ||
[] | ||
); | ||
|
||
ruleTester.run('prefer-expect-query-by', rule, { | ||
valid: queryByVariants.reduce( | ||
(validRules, queryName) => [ | ||
...validRules, | ||
{ code: `expect(${queryName}('Hello')).toBeInTheDocument()` }, | ||
{ code: `expect(rendered.${queryName}('Hello')).toBeInTheDocument()` }, | ||
{ code: `expect(${queryName}('Hello')).not.toBeInTheDocument()` }, | ||
{ | ||
code: `expect(rendered.${queryName}('Hello')).not.toBeInTheDocument()`, | ||
}, | ||
], | ||
[] | ||
), | ||
invalid: getByVariants.reduce( | ||
(invalidRules, queryName) => [ | ||
...invalidRules, | ||
{ | ||
code: `expect(${queryName}('Hello')).toBeInTheDocument()`, | ||
errors: [{ messageId: 'expectQueryBy' }], | ||
}, | ||
{ | ||
code: `expect(rendered.${queryName}('Hello')).toBeInTheDocument()`, | ||
errors: [{ messageId: 'expectQueryBy' }], | ||
}, | ||
{ | ||
code: `expect(${queryName}('Hello')).not.toBeInTheDocument()`, | ||
errors: [{ messageId: 'expectQueryBy' }], | ||
}, | ||
{ | ||
code: `expect(rendered.${queryName}('Hello')).not.toBeInTheDocument()`, | ||
errors: [{ messageId: 'expectQueryBy' }], | ||
}, | ||
], | ||
[] | ||
), | ||
}); | ||
Belco90 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You also need to add some tests for autofixing feature, you can find some examples in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah didn't know that. I added it now. Thanks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some tests are going to be needed for sure, something like that: {
code: `
const message = getByText('Hello')
expect(message).toBeInTheDocument()
`,
errors: [{ messageId: 'expectQueryBy' }],
output: `
const message = queryByText('Hello')
expect(message).toBeInTheDocument()
`,
}, If auto-fixing is complex technically, I think we can leave it as an improvement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thomlom those specific tests for matched elements saved in a var are not necessary, but I found an issue with the current autofix so probably we need to leave it as an improvement. @emmenko you are fixing the query used inside expect statement, but you also need to take care about if the query is imported or not. Let's say I have this test:
would be replaced by
but it's not 100% fixed as And another case:
would be replaced by
so here So here we have to 1) improve the fix function to take care of the new query being defined or not or 2) ignore fixing the rule for now. I'm happy with option 2 and leave the autofix for future improvement as implement the proper autofix here could take longer than expected. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixing rules do not need to account for other linting errors. As stated in the eslint docs
So from my understanding, this is totally fine how the rule autofix is implemented regarding its concerns. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I understand what you mean, but even if it's explained in the rule info I don't want to leave this autofix with the possibility of breaking the code as it would harm the developer experience. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I totally get what you mean and I agree on the fact that fixing rules are only useful the first time. But this plugin is new and the testing library's ecosystem is still growing. There are some best practices emerging but I think it's still not clear for newcomers to get how to make the most of Testing Library. My concern is that people will rely on this plugin to implement best practices for them automatically, possibly break their tests and have a bad experience with it.
I prefer to have a limited experience that leaves you the manual work instead of a full experience that might cause your code to break. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So you want to remove/disable the autofix for now? It's fine by me, I just wanted to share my perspective on this topic before we decide on how to proceed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course! This is not about what I want or something like that :) really happy to have your point of view here. Better to remove the fixing implementation (including the badge and the output in the tests, sorry about the last one as I asked that), and think about it in a future issue. For first rules we added to the plugin we also skipped the autofix for most of them as there are some cases we didn't know what to do or it took longer than expected to be implemented, so it's better to go bit by bit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's disabled here: 0fd4a6c I kept it commented out, so that we have a starting point in case we want to pick it up in the future. |
Uh oh!
There was an error while loading. Please reload this page.