Skip to content

Commit

Permalink
feat(rule): prefer expect queryBy (#22)
Browse files Browse the repository at this point in the history
* feat(rule): prefer expect queryBy

* chore: regenerate lockfile

* refactor: improve docs and tests, include also findBy

* refactor: remove occurrences of findBy

* refactor: generate test cases for all query methods

* refactor: disable autofix implementation

* refactor: remove fixable badge

* refactor: fixable option to null
  • Loading branch information
emmenko authored and Belco90 committed Oct 16, 2019
1 parent b0c7ace commit 260f849
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 8 deletions.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,14 @@ 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][] |
| 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][] | |

[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
61 changes: 61 additions & 0 deletions docs/rules/prefer-expect-query-by.md
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)
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ 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'),
'prefer-expect-query-by': require('./rules/prefer-expect-query-by'),
};

const recommendedRules = {
'testing-library/await-async-query': 'error',
'testing-library/no-await-sync-query': 'error',
'testing-library/prefer-expect-query-by': 'error',
};

module.exports = {
Expand Down
102 changes: 102 additions & 0 deletions lib/rules/prefer-expect-query-by.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

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

const AST_NODE_TYPES = {
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*)`
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')
// );
// },
});
}
},
}),
};
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tests/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Object {
"error",
"angular",
],
"testing-library/prefer-expect-query-by": "error",
},
}
`;
Expand All @@ -30,6 +31,7 @@ Object {
"error",
"react",
],
"testing-library/prefer-expect-query-by": "error",
},
}
`;
Expand All @@ -42,6 +44,7 @@ Object {
"rules": Object {
"testing-library/await-async-query": "error",
"testing-library/no-await-sync-query": "error",
"testing-library/prefer-expect-query-by": "error",
},
}
`;
Expand All @@ -60,6 +63,7 @@ Object {
"error",
"vue",
],
"testing-library/prefer-expect-query-by": "error",
},
}
`;
58 changes: 58 additions & 0 deletions tests/lib/rules/prefer-expect-query-by.js
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' }],
},
],
[]
),
});

0 comments on commit 260f849

Please sign in to comment.