-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(prefer-implicit-assert): adding new rule (#815)
Closes #743 * feat(prefer-implicit-assert): adding new rule * feat(prefer-implicit-assert): increment number of rules * feat(prefer-implicit-assert): reduce duplication * feat(prefer-implicit-assert): add recommened rule by library, more test cases, update docs * feat(prefer-implicit-assert): added findBy link * feat(prefer-implicit-assert): added line and column checks * feat(prefer-implicit-assert): use existing utils * feat(prefer-implicit-assert): remove unnecessary checks
- Loading branch information
Showing
6 changed files
with
751 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Suggest using implicit assertions for getBy* & findBy* queries (`testing-library/prefer-implicit-assert`) | ||
|
||
<!-- end auto-generated rule header --> | ||
|
||
Testing Library `getBy*` & `findBy*` queries throw an error if the element is not | ||
found. Therefore it is not necessary to also assert existance with things like `expect(getBy*.toBeInTheDocument()` or `expect(awaint findBy*).not.toBeNull()` | ||
|
||
## Rule Details | ||
|
||
This rule aims to reuduce uncecessary assertion's for presense of an element, | ||
when using queries that implicitly fail when said element is not found. | ||
|
||
Examples of **incorrect** code for this rule with the default configuration: | ||
|
||
```js | ||
// wrapping the getBy or findBy queries within a `expect` and using existence matchers for | ||
// making the assertion is not necessary | ||
expect(getByText('foo')).toBeInTheDocument(); | ||
expect(await findByText('foo')).toBeInTheDocument(); | ||
|
||
expect(getByText('foo')).toBeDefined(); | ||
expect(await findByText('foo')).toBeDefined(); | ||
|
||
const utils = render(<Component />); | ||
expect(utils.getByText('foo')).toBeInTheDocument(); | ||
expect(await utils.findByText('foo')).toBeInTheDocument(); | ||
|
||
expect(await findByText('foo')).not.toBeNull(); | ||
expect(await findByText('foo')).not.toBeUndified(); | ||
``` | ||
|
||
Examples of **correct** code for this rule with the default configuration: | ||
|
||
```js | ||
getByText('foo'); | ||
await findByText('foo'); | ||
|
||
const utils = render(<Component />); | ||
utils.getByText('foo'); | ||
await utils.findByText('foo'); | ||
|
||
// When using queryBy* queries thees do not implicitly fial therefore you should explicitly check if your elements eixst or not | ||
expect(queryByText('foo')).toBeInTheDocument(); | ||
expect(queryByText('foo')).not.toBeInTheDocument(); | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
If you prefer to use `getBy*` & `findBy*` queries with explicitly asserting existence of elements, then this rule is not recommended. Instead check out this rule [prefer-explicit-assert](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-explicit-assert.md) | ||
|
||
- Never use both `prefer-implicit-assert` & `prefer-explicit-assert` choose one. | ||
- This library recommends `prefer-explicit-assert` to make it more clear to readers that it is not just a query without an assertion, but that it is checking for existence of an element | ||
|
||
## Further Reading | ||
|
||
- [getBy query](https://testing-library.com/docs/dom-testing-library/api-queries#getby) | ||
- [findBy query](https://testing-library.com/docs/dom-testing-library/api-queries#findBy) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { | ||
TSESTree, | ||
ASTUtils, | ||
AST_NODE_TYPES, | ||
TSESLint, | ||
} from '@typescript-eslint/utils'; | ||
|
||
import { createTestingLibraryRule } from '../create-testing-library-rule'; | ||
import { TestingLibrarySettings } from '../create-testing-library-rule/detect-testing-library-utils'; | ||
import { isCallExpression, isMemberExpression } from '../node-utils'; | ||
|
||
export const RULE_NAME = 'prefer-implicit-assert'; | ||
export type MessageIds = 'preferImplicitAssert'; | ||
type Options = []; | ||
|
||
const isCalledUsingSomeObject = (node: TSESTree.Identifier) => | ||
isMemberExpression(node.parent) && | ||
node.parent.object.type === AST_NODE_TYPES.Identifier; | ||
|
||
const isCalledInExpect = ( | ||
node: TSESTree.Identifier | TSESTree.Node, | ||
isAsyncQuery: boolean | ||
) => { | ||
if (isAsyncQuery) { | ||
return ( | ||
isCallExpression(node.parent) && | ||
ASTUtils.isAwaitExpression(node.parent.parent) && | ||
isCallExpression(node.parent.parent.parent) && | ||
ASTUtils.isIdentifier(node.parent.parent.parent.callee) && | ||
node.parent.parent.parent.callee.name === 'expect' | ||
); | ||
} | ||
return ( | ||
isCallExpression(node.parent) && | ||
isCallExpression(node.parent.parent) && | ||
ASTUtils.isIdentifier(node.parent.parent.callee) && | ||
node.parent.parent.callee.name === 'expect' | ||
); | ||
}; | ||
|
||
const reportError = ( | ||
context: Readonly< | ||
TSESLint.RuleContext<'preferImplicitAssert', []> & { | ||
settings: TestingLibrarySettings; | ||
} | ||
>, | ||
node: TSESTree.Identifier | TSESTree.Node | undefined, | ||
queryType: string | ||
) => { | ||
if (node) { | ||
return context.report({ | ||
node, | ||
messageId: 'preferImplicitAssert', | ||
data: { | ||
queryType, | ||
}, | ||
}); | ||
} | ||
}; | ||
|
||
export default createTestingLibraryRule<Options, MessageIds>({ | ||
name: RULE_NAME, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: | ||
'Suggest using implicit assertions for getBy* & findBy* queries', | ||
recommendedConfig: { | ||
dom: false, | ||
angular: false, | ||
react: false, | ||
vue: false, | ||
marko: false, | ||
}, | ||
}, | ||
messages: { | ||
preferImplicitAssert: | ||
"Don't wrap `{{queryType}}` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `{{queryType}}` queries fail implicitly when element is not found", | ||
}, | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context, _, helpers) { | ||
const findQueryCalls: TSESTree.Identifier[] = []; | ||
const getQueryCalls: TSESTree.Identifier[] = []; | ||
|
||
return { | ||
'CallExpression Identifier'(node: TSESTree.Identifier) { | ||
if (helpers.isFindQueryVariant(node)) { | ||
findQueryCalls.push(node); | ||
} | ||
if (helpers.isGetQueryVariant(node)) { | ||
getQueryCalls.push(node); | ||
} | ||
}, | ||
'Program:exit'() { | ||
findQueryCalls.forEach((queryCall) => { | ||
const isAsyncQuery = true; | ||
const node: TSESTree.Identifier | TSESTree.Node | undefined = | ||
isCalledUsingSomeObject(queryCall) ? queryCall.parent : queryCall; | ||
|
||
if (node) { | ||
if (isCalledInExpect(node, isAsyncQuery)) { | ||
if ( | ||
isMemberExpression(node.parent?.parent?.parent?.parent) && | ||
node.parent?.parent?.parent?.parent.property.type === | ||
AST_NODE_TYPES.Identifier && | ||
helpers.isPresenceAssert(node.parent.parent.parent.parent) | ||
) { | ||
return reportError(context, node, 'findBy*'); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
getQueryCalls.forEach((queryCall) => { | ||
const isAsyncQuery = false; | ||
const node: TSESTree.Identifier | TSESTree.Node | undefined = | ||
isCalledUsingSomeObject(queryCall) ? queryCall.parent : queryCall; | ||
if (node) { | ||
if (isCalledInExpect(node, isAsyncQuery)) { | ||
if ( | ||
isMemberExpression(node.parent?.parent?.parent) && | ||
node.parent?.parent?.parent.property.type === | ||
AST_NODE_TYPES.Identifier && | ||
helpers.isPresenceAssert(node.parent.parent.parent) | ||
) { | ||
return reportError(context, node, 'getBy*'); | ||
} | ||
} | ||
} | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.