Skip to content
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,15 @@ To enable this configuration use the `extends` property in your

## Supported Rules

| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md) | Disallow the use of `expect(getBy*)` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md) | Disallow the use of `expect(getBy*)` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |

[build-badge]: https://img.shields.io/travis/Belco90/eslint-plugin-testing-library?style=flat-square
[build-url]: https://travis-ci.org/belco90/eslint-plugin-testing-library
Expand Down
83 changes: 83 additions & 0 deletions docs/rules/prefer-explicit-assert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Suggest using explicit assertions rather than just `getBy*` queries (prefer-explicit-assert)

Testing Library `getBy*` queries throw an error if the element is not
found. Some users like this behavior to use the query itself as an
assert for the element existence in their tests, but other users don't
and prefer to explicitly assert the element existence, so this rule is
for users from the latter.

## Rule Details

This rule aims to encourage users to explicitly assert existence of
elements in their tests rather than just use `getBy*` queries and expect
it doesn't throw an error so it's easier to understand what's the
expected behavior within the test.

> ⚠️ Please note that this rule is recommended to be used together with
> [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md), so the
> combination of these two rules will force users to do 2 actual
> changes: wrap `getBy*` with `expect` assertion and then use `queryBy*`
> instead of `getBy*` for asserting.

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

```js
// just calling `getBy*` query expecting not to throw an error as an
// assert-like method, without actually either using the returned element
// or explicitly asserting
getByText('foo');

const utils = render(<Component />);
utils.getByText('foo');
```

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

```js
// wrapping the get query within a `expect` and use some matcher for
// making the assertion more explicit
expect(getByText('foo')).toBeDefined();

const utils = render(<Component />);
expect(utils.getByText('foo')).toBeDefined();

// ⚠️ `getBy*` should be replaced by `queryBy*` when combined with `prefer-expect-query-by` rule
expect(queryByText('foo')).toBeDefined();

// even more explicit if you use `@testing-library/jest-dom` matcher
// for checking the element is present in the document
expect(queryByText('foo')).toBeInTheDocument();

// Doing something with the element returned without asserting is absolutely fine
await waitForElement(() => getByText('foo'));
fireEvent.click(getByText('bar'));
const quxElement = getByText('qux');

// call directly something different than Testing Library query
getByNonTestingLibraryVariant('foo');
```

## Options

This rule accepts a single options argument:

- `customQueryNames`: this array option allows to extend default Testing
Library queries with custom ones for including them into rule
inspection.

```js
"testing-library/prefer-expect-query-by": ["error", {"customQueryNames": ["getByIcon", "getBySomethingElse"]}],
```

## When Not To Use It

If you prefer to use `getBy*` queries implicitly as an assert-like
method itself, then this rule is not recommended.

## Related Rules

- [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md)

## Further Reading

- [getBy query](https://testing-library.com/docs/dom-testing-library/api-queries#getby)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const rules = {
'no-debug': require('./rules/no-debug'),
'no-dom-import': require('./rules/no-dom-import'),
'prefer-expect-query-by': require('./rules/prefer-expect-query-by'),
'prefer-explicit-assert': require('./rules/prefer-explicit-assert'),
};

const recommendedRules = {
Expand Down
79 changes: 79 additions & 0 deletions lib/rules/prefer-explicit-assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict';

const { getDocsUrl, ALL_QUERIES_METHODS } = require('../utils');

const ALL_GET_BY_QUERIES = ALL_QUERIES_METHODS.map(
queryMethod => `get${queryMethod}`
);

const findCallExpressionParent = node =>
node.type === 'CallExpression' ? node : findCallExpressionParent(node.parent);

const isValidQuery = (node, customQueryNames = []) =>
ALL_GET_BY_QUERIES.includes(node.name) ||
customQueryNames.includes(node.name);

const isDirectlyCalledByFunction = node =>
node.parent.type === 'CallExpression';

const isReturnedByArrowFunctionExpression = node =>
node.parent.type === 'ArrowFunctionExpression';

const isDeclared = node => node.parent.type === 'VariableDeclarator';

const isReturnedByReturnStatement = node =>
node.parent.type === 'ReturnStatement';

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Suggest using explicit assertions rather than just `getBy*` queries',
category: 'Best Practices',
recommended: false,
url: getDocsUrl('prefer-explicit-assert'),
},
messages: {
preferExplicitAssert:
'Wrap stand-alone `getBy*` query with `expect` function for better explicit assertion',
},
fixable: null,
schema: [
{
type: 'object',
properties: {
customQueryNames: {
type: 'array',
},
},
},
],
},

create: function(context) {
return {
'CallExpression Identifier'(node) {
const callExpressionNode = findCallExpressionParent(node);

let customQueryNames;
if (context.options && context.options.length > 0) {
[{ customQueryNames }] = context.options;
}

if (
isValidQuery(node, customQueryNames) &&
!isDirectlyCalledByFunction(callExpressionNode) &&
!isReturnedByArrowFunctionExpression(callExpressionNode) &&
!isDeclared(callExpressionNode) &&
!isReturnedByReturnStatement(callExpressionNode)
) {
context.report({
node,
messageId: 'preferExplicitAssert',
});
}
},
};
},
};
110 changes: 110 additions & 0 deletions tests/lib/rules/prefer-explicit-assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';

const rule = require('../../../lib/rules/prefer-explicit-assert');
const { ALL_QUERIES_METHODS } = require('../../../lib/utils');
const RuleTester = require('eslint').RuleTester;

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
ruleTester.run('prefer-explicit-assert', rule, {
valid: [
{
code: `getByText`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also check for when queries are not destructured?

const rendered = render(<App />)

rendered.getByText('...')
// ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice spot! This actually found some issues in the rule, so I'm adding those tests and fixing the rule implementation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added, rule fixed and more examples added to the doc

},
{
code: `const utils = render()

utils.getByText
`,
},
{
code: `expect(getByText('foo')).toBeDefined()`,
},
{
code: `const utils = render()

expect(utils.getByText('foo')).toBeDefined()
`,
},
{
code: `expect(getByText('foo')).toBeInTheDocument();`,
},
{
code: `async () => { await waitForElement(() => getByText('foo')) }`,
},
{
code: `fireEvent.click(getByText('bar'));`,
},
{
code: `const quxElement = getByText('qux')`,
},
{
code: `() => { return getByText('foo') }`,
},
{
code: `function bar() { return getByText('foo') }`,
},
{
code: `getByIcon('foo')`, // custom `getBy` query not extended through options
},
],

invalid: [
...ALL_QUERIES_METHODS.map(queryMethod => ({
code: `get${queryMethod}('foo')`,
errors: [
{
messageId: 'preferExplicitAssert',
},
],
})),
...ALL_QUERIES_METHODS.map(queryMethod => ({
code: `const utils = render()

utils.get${queryMethod}('foo')`,
errors: [
{
messageId: 'preferExplicitAssert',
line: 3,
column: 13,
},
],
})),
...ALL_QUERIES_METHODS.map(queryMethod => ({
code: `() => {
get${queryMethod}('foo')
doSomething()

get${queryMethod}('bar')
const quxElement = get${queryMethod}('qux')
}
`,
errors: [
{
messageId: 'preferExplicitAssert',
line: 2,
},
{
messageId: 'preferExplicitAssert',
line: 5,
},
],
})),
{
code: `getByIcon('foo')`, // custom `getBy` query extended through options
options: [
{
customQueryNames: ['getByIcon'],
},
],
errors: [
{
messageId: 'preferExplicitAssert',
},
],
},
],
});