Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4ec21b7
feat: added prefer enabled disabled
Oct 23, 2019
fd86cdb
fix: unit test and updated docs
Oct 23, 2019
f089edc
feat: added toHaveAttribute
Oct 24, 2019
717040f
feat: support for not
Oct 24, 2019
68e0daf
feat: don't use not on toBeDisabled/Enabled
Oct 24, 2019
59992f9
fix: little refactoring
Oct 24, 2019
e02f9f2
chore: addressed PR feedback
Oct 24, 2019
fcd267a
fix: added url
Oct 24, 2019
3488c5d
docs: added to home page
Oct 24, 2019
c8bdc40
docs: changed prefer enabled disabled to recommended
Oct 24, 2019
4db94eb
docs: added to recommended
Oct 24, 2019
db6cef3
docs: fixed typo in readme
Oct 24, 2019
a9a2563
docs: more docs
Oct 24, 2019
aff8bc6
Merge branch 'master' into feature/prefer-enabled-disabled
benmonro Oct 24, 2019
86f4972
chore: removed comments
Oct 24, 2019
cd83ab4
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
5e6ca6c
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
9c72cd3
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
9ae4277
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
2a86e3a
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
4cceff7
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
d0fc8e6
Update docs/rules/jest-dom-prefer-enabled-disabled.md
benmonro Oct 25, 2019
ca1fb14
chore: addressed PR feedback
Oct 25, 2019
139f88f
fix: added another valid test
Oct 25, 2019
ca796a8
feat: added rules for checked & required
Oct 27, 2019
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,16 @@ 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][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| 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][] | |
| [jest-dom-prefer-enabled-disabled](docs/rules/jest-dom-prefer-enabled-disabled.md) | Disallow the use of `toHaveProperty('disabled')` and `toHaveAttribute('disabled')` | | ![fixable-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
76 changes: 76 additions & 0 deletions docs/rules/jest-dom-prefer-enabled-disabled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# prefer toBeDisabled() or toBeEnabled() over toHaveProperty('disabled', true|false) (jest-dom-prefer-enabled-disabled)

## Rule Details

This rule aims to prevent false positives and improve readability and should only be used with the `@testing-library/jest-dom` package. See below for examples of those potential issues and why this rule is recommended. The rule is autofixable and will replace any instances of `.toHaveProperty()` or `.toHaveAttribute()` with `.toBeEnabled()` or `toBeDisabled()` as appropriate.

In addition, to avoid double negatives and confusing syntax, `expect(element).not.toBeDisabled()` is also reported and auto-fixed to `expect(element).toBeEnabled()` and vice versa.

### False positives

Consider these 2 snippets:

```js
const { getByRole } = render(<input type="checkbox" disabled />);
const element = getByRole('checkbox');
expect(element).toHaveProperty('disabled'); // passes

const { getByRole } = render(<input type="checkbox" />);
const element = getByRole('checkbox');
expect(element).toHaveProperty('disabled'); // also passes 😱
```

### Readability

Consider the following snippets:

```js
const { getByRole } = render(<input type="checkbox" />);
const element = getByRole('checkbox');

expect(element).toHaveAttribute('disabled', false); // fails
expect(element).toHaveAttribute('disabled', ''); // fails
expect(element).not.toHaveAttribute('disabled', ''); // passes

expect(element).not.toHaveAttribute('disabled', true); // passes.
expect(element).not.toHaveAttribute('disabled', false); // also passes.
```

As you can see, using `toHaveAttribute` in this case is confusing, unintuitive and can even lead to false positive tests.

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

```js
expect(element).toHaveProperty('disabled', true);
expect(element).toHaveAttribute('disabled', false);

expect(element).toHaveAttribute('disabled');
expect(element).not.toHaveProperty('disabled');

expect(element).not.toBeDisabled();
expect(element).not.toBeEnabled();
```

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

```js
expect(element).toBeEnabled();

expect(element).toBeDisabled();

expect(element).toHaveProperty('checked', true);

expect(element).toHaveAttribute('checked');
```

## When Not To Use It

Don't use this rule if you:

- don't use `jest-dom`
- want to allow `.toHaveProperty('disabled', true|false);`

## Further reading

- [toBeDisabled](https://github.com/testing-library/jest-dom#tobedisabled)
- [toBeEnabled](https://github.com/testing-library/jest-dom#tobeenabled)
11 changes: 11 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
module.exports = {
testMatch: ['**/tests/**/*.js'],
collectCoverage: true,
coverageThreshold: {
global: {
branches: 96.55,
functions: 100,
lines: 98.97,
statements: 0,
},
},
testPathIgnorePatterns: ['<rootDir>/tests/fixtures/'],
collectCoverageFrom: ['lib/**/*.js', '!**/node_modules/**'],
};
81 changes: 81 additions & 0 deletions lib/createBannedAttributeRule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module.exports = ({ preferred, negatedPreferred, attributes }) => context => {
function getCorrectFunctionFor(node, negated = false) {
return (node.arguments.length === 1 ||
node.arguments[1].value === true ||
node.arguments[1].value === '') &&
!negated
? preferred
: negatedPreferred;
}

const isBannedArg = node =>
attributes.some(attr => attr === node.arguments[0].value);

return {
[`CallExpression[callee.property.name=/${preferred}|${negatedPreferred}/][callee.object.property.name='not'][callee.object.object.callee.name='expect']`](
node
) {
if (negatedPreferred.startsWith('toBe')) {
const incorrectFunction = node.callee.property.name;

const correctFunction =
incorrectFunction === preferred ? negatedPreferred : preferred;
context.report({
message: `Use ${correctFunction}() instead of not.${incorrectFunction}()`,
node,
fix(fixer) {
return fixer.replaceTextRange(
[node.callee.object.property.start, node.end],
`${correctFunction}()`
);
},
});
}
},
"CallExpression[callee.property.name=/toHaveProperty|toHaveAttribute/][callee.object.property.name='not'][callee.object.object.callee.name='expect']"(
node
) {
const arg = node.arguments[0].value;
if (isBannedArg(node)) {
const correctFunction = getCorrectFunctionFor(node, true);

const incorrectFunction = node.callee.property.name;
context.report({
message: `Use ${correctFunction}() instead of not.${incorrectFunction}('${arg}')`,
node,
fix(fixer) {
return fixer.replaceTextRange(
[node.callee.object.property.start, node.end],
`${correctFunction}()`
);
},
});
}
},
"CallExpression[callee.object.callee.name='expect'][callee.property.name=/toHaveProperty|toHaveAttribute/]"(
node
) {
if (isBannedArg(node)) {
const correctFunction = getCorrectFunctionFor(node);

const incorrectFunction = node.callee.property.name;

const message = `Use ${correctFunction}() instead of ${incorrectFunction}(${node.arguments
.map(({ raw }) => raw)
.join(', ')})`;
context.report({
node: node.callee.property,
message,
fix(fixer) {
return [
fixer.replaceTextRange(
[node.callee.property.start, node.end],
`${correctFunction}()`
),
];
},
});
}
},
};
};
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
const rules = {
'await-async-query': require('./rules/await-async-query'),
'await-fire-event': require('./rules/await-fire-event'),
'jest-dom-prefer-checked': require('./rules/jest-dom-prefer-checked'),
'jest-dom-prefer-enabled-disabled': require('./rules/jest-dom-prefer-enabled-disabled'),
'jest-dom-prefer-required': require('./rules/jest-dom-prefer-required'),
'no-await-sync-query': require('./rules/no-await-sync-query'),
'no-debug': require('./rules/no-debug'),
'no-dom-import': require('./rules/no-dom-import'),
Expand Down
27 changes: 27 additions & 0 deletions lib/rules/jest-dom-prefer-checked.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
* @author Ben Monro
*/
'use strict';

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

module.exports = {
meta: {
docs: {
description:
'prefer toBeDisabled or toBeEnabled over checking properties',
category: 'jest-dom',
recommended: false,
url: getDocsUrl('jest-dom-prefer-checked'),
},
fixable: 'code',
},

create: createBannedAttributeRule({
preferred: 'toBeChecked',
negatedPreferred: 'not.toBeChecked',
attributes: ['checked', 'aria-checked'],
}),
};
27 changes: 27 additions & 0 deletions lib/rules/jest-dom-prefer-enabled-disabled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a purpose to include these JSDocs? The filename seems enough to me 🙂

* @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
* @author Ben Monro
*/
'use strict';

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

module.exports = {
meta: {
docs: {
description:
'prefer toBeDisabled or toBeEnabled over checking properties',
category: 'jest-dom',
recommended: false,
url: getDocsUrl('jest-dom-prefer-enabled-disabled'),
},
fixable: 'code',
},

create: createBannedAttributeRule({
preferred: 'toBeDisabled',
negatedPreferred: 'toBeEnabled',
attributes: ['disabled'],
}),
};
27 changes: 27 additions & 0 deletions lib/rules/jest-dom-prefer-required.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
* @author Ben Monro
*/
'use strict';

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

module.exports = {
meta: {
docs: {
description:
'prefer toBeDisabled or toBeEnabled over checking properties',
category: 'jest-dom',
recommended: false,
url: getDocsUrl('jest-dom-prefer-required'),
},
fixable: 'code',
},

create: createBannedAttributeRule({
preferred: 'toBeRequired',
negatedPreferred: 'not.toBeRequired',
attributes: ['required', 'aria-required'],
}),
};
Loading