Skip to content

feat: new rule prefer-presence-queries #98

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

Merged
merged 10 commits into from
Mar 28, 2020
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,21 +134,21 @@ 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-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![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][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
| [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][] |
| [no-get-by-for-checking-element-not-present](docs/rules/no-get-by-for-checking-element-not-present.md) | Disallow the use of `getBy*` queries when checking elements are not present | | |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-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-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![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][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
| [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][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |

[build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square
[build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library
Expand Down
63 changes: 0 additions & 63 deletions docs/rules/no-get-by-for-checking-element-not-present.md

This file was deleted.

67 changes: 67 additions & 0 deletions docs/rules/prefer-presence-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Enforce specific queries when checking element is present or not (prefer-presence-queries)

The (DOM) Testing Library allows to query DOM elements using different types of queries such as `get*` and `query*`. Using `get*` throws an error in case the element is not found, while `query*` returns null instead of throwing (or empty array for `queryAllBy*` ones). These differences are useful in some situations:

- using `getBy*` queries when asserting if element is present, so if the element is not found the error thrown will offer better info than asserting with other queries which will not throw an error.
- using `queryBy*` queries when asserting if element is not present, so the test doesn't fail immediately when the element is not found and the assertion can be executed properly.

## Rule details

This rule fires whenever:

- `queryBy*` or `queryAllBy*` are used to assert element **is** present with `.toBeInTheDocument()`, `toBeTruthy()` or `.toBeDefined()` matchers or negated matchers from case below.
- `getBy*` or `getAllBy*` are used to assert element **is not** present with `.toBeNull()` or `.toBeFalsy()` matchers or negated matchers from case above.

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

```js
test('some test', () => {
render(<App />);

// check element is present with `queryBy*`
expect(screen.queryByText('button')).toBeInTheDocument();
expect(screen.queryAllByText('button')[0]).toBeTruthy();
expect(screen.queryByText('button')).toBeNull();
expect(screen.queryAllByText('button')[2]).not.toBeNull();
expect(screen.queryByText('button')).not.toBeFalsy();

// check element is NOT present with `getBy*`
expect(screen.getByText('loading')).not.toBeInTheDocument();
expect(screen.getAllByText('loading')[1]).not.toBeTruthy();
expect(screen.getByText('loading')).not.toBeNull();
expect(screen.getAllByText('loading')[3]).toBeNull();
expect(screen.getByText('loading')).toBeFalsy();
});
```

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

```js
test('some test', async () => {
render(<App />);
// check element is present with `getBy*`
expect(screen.getByText('button')).toBeInTheDocument();
expect(screen.getAllByText('button')[9]).toBeTruthy();
expect(screen.getByText('button')).toBeNull();
expect(screen.getAllByText('button')[7]).not.toBeNull();
expect(screen.getByText('button')).not.toBeFalsy();

// check element is NOT present with `queryBy*`
expect(screen.queryByText('loading')).not.toBeInTheDocument();
expect(screen.queryAllByText('loading')[8]).not.toBeTruthy();
expect(screen.queryByText('loading')).not.toBeNull();
expect(screen.queryAllByText('loading')[6]).toBeNull();
expect(screen.queryByText('loading')).toBeFalsy();

// `findBy*` queries are out of the scope for this rule
const button = await screen.findByText('submit');
expect(button).toBeInTheDocument();
});
```

## Further Reading

- [Testing Library queries cheatsheet](https://testing-library.com/docs/dom-testing-library/cheatsheet#queries)
- [Asserting elements are not present](https://testing-library.com/docs/guide-disappearance#asserting-elements-are-not-present)
- [jest-dom note about using `getBy` within assertions](https://testing-library.com/docs/ecosystem-jest-dom)
- [Waiting for appearance](https://testing-library.com/docs/guide-disappearance#waiting-for-appearance)
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const rules = {
'no-await-sync-query': require('./rules/no-await-sync-query'),
'no-debug': require('./rules/no-debug'),
'no-dom-import': require('./rules/no-dom-import'),
'no-get-by-for-checking-element-not-present': require('./rules/no-get-by-for-checking-element-not-present'),
'no-manual-cleanup': require('./rules/no-manual-cleanup'),
'no-wait-for-empty-callback': require('./rules/no-wait-for-empty-callback'),
'prefer-explicit-assert': require('./rules/prefer-explicit-assert'),
'prefer-presence-queries': require('./rules/prefer-presence-queries'),
'prefer-screen-queries': require('./rules/prefer-screen-queries'),
'prefer-wait-for': require('./rules/prefer-wait-for'),
};
Expand Down
83 changes: 0 additions & 83 deletions lib/rules/no-get-by-for-checking-element-not-present.js

This file was deleted.

94 changes: 94 additions & 0 deletions lib/rules/prefer-presence-queries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';

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

const QUERIES_REGEXP = new RegExp(
`^(get|query)(All)?(${ALL_QUERIES_METHODS.join('|')})$`
);
const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined'];
const ABSENCE_MATCHERS = ['toBeNull', 'toBeFalsy'];

module.exports = {
meta: {
docs: {
category: 'Best Practices',
description:
'Ensure appropriate get*/query* queries are used with their respective matchers',
recommended: 'error',
url: getDocsUrl('prefer-presence-queries'),
},
messages: {
presenceQuery:
'Use `getBy*` queries rather than `queryBy*` for checking element is present',
absenceQuery:
'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present',
expectQueryBy:
'Use `getBy*` only when checking elements are present, otherwise use `queryBy*`',
},
schema: [],
type: 'suggestion',
fixable: null,
},

create: context => ({
[`CallExpression Identifier[name=${QUERIES_REGEXP}]`](node) {
const expectCallNode = findClosestCallNode(node, 'expect');

if (expectCallNode) {
const expectStatement = expectCallNode.parent;
let matcher = expectStatement.property && expectStatement.property.name;
let isNegatedMatcher = false;

if (matcher === 'not') {
isNegatedMatcher = true;
matcher =
expectStatement.parent &&
expectStatement.parent.property &&
expectStatement.parent.property.name;
}

if (!matcher) {
return;
}

const validMatchers = isThrowingQuery(node)
? PRESENCE_MATCHERS
: ABSENCE_MATCHERS;

const invalidMatchers = isThrowingQuery(node)
? ABSENCE_MATCHERS
: PRESENCE_MATCHERS;

const messageId = isThrowingQuery(node)
? 'absenceQuery'
: 'presenceQuery';

if (
(!isNegatedMatcher && invalidMatchers.includes(matcher)) ||
(isNegatedMatcher && validMatchers.includes(matcher))
) {
return context.report({
node,
messageId,
});
}
}
},
}),
};

function isThrowingQuery(node) {
return node.name.startsWith('get');
}

function findClosestCallNode(node, name) {
if (!node.parent) {
return false;
}

if (node.type === 'CallExpression' && node.callee.name === name) {
return node;
} else {
return findClosestCallNode(node.parent, name);
}
}
Loading