Skip to content

Commit 0823052

Browse files
author
Ben Monro
committed
feat: added 3 rules
0 parents  commit 0823052

18 files changed

+637
-0
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
coverage/

.eslintrc.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"parserOptions": {
3+
"ecmaVersion": 2018
4+
},
5+
"env": {
6+
"commonjs": true,
7+
"es6": true,
8+
"node": true
9+
// "jest/globals": true
10+
},
11+
"plugins": [
12+
"eslint-plugin",
13+
"prettier"
14+
],
15+
"extends": [
16+
"plugin:eslint-plugin/recommended"
17+
],
18+
"rules": {
19+
"prettier/prettier": "error",
20+
"no-var": "error"
21+
}
22+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
coverage
3+
yarn.lock
4+
package-lock.json

.prettierrc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"trailingComma": "es5",
3+
"singleQuote": true
4+
}

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# eslint-plugin-jest-dom
2+
3+
lint rules for use with jest-dom
4+
5+
## Installation
6+
7+
You'll first need to install [ESLint](http://eslint.org):
8+
9+
```
10+
$ npm i eslint --save-dev
11+
```
12+
13+
Next, install `eslint-plugin-jest-dom`:
14+
15+
```
16+
$ npm install eslint-plugin-jest-dom --save-dev
17+
```
18+
19+
**Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-jest-dom` globally.
20+
21+
## Usage
22+
23+
Add `jest-dom` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
24+
25+
```json
26+
{
27+
"plugins": [
28+
"jest-dom"
29+
]
30+
}
31+
```
32+
33+
34+
Then configure the rules you want to use under the rules section.
35+
36+
```json
37+
{
38+
"rules": {
39+
"jest-dom/prefer-checked": "error",
40+
"jest-dom/prefer-enabled-disabled": "error",
41+
"jest-dom/prefer-required": "error"
42+
}
43+
}
44+
```
45+
46+
## Supported Rules
47+
48+
✔️ indicates that a rule is recommended for all users.
49+
50+
🛠 indicates that a rule is fixable.
51+
52+
<!-- __BEGIN AUTOGENERATED TABLE__ -->
53+
Name | ✔️ | 🛠 | Description
54+
----- | ----- | ----- | -----
55+
[prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | ✔️ | 🛠 | prefer toBeChecked over checking attributes
56+
[prefer-enabled-disabled](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-enabled-disabled.md) | ✔️ | 🛠 | prefer toBeDisabled or toBeEnabled over checking attributes
57+
[prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | ✔️ | 🛠 | prefer toBeRequired over checking properties
58+
<!-- __END AUTOGENERATED TABLE__ -->
59+
60+
61+
62+
63+

build/generate-readme-table.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const rules = require('..').rules;
6+
7+
const README_LOCATION = path.resolve(__dirname, '..', 'README.md');
8+
const BEGIN_TABLE_MARKER = '<!-- __BEGIN AUTOGENERATED TABLE__ -->\n';
9+
const END_TABLE_MARKER = '\n<!-- __END AUTOGENERATED TABLE__ -->';
10+
11+
const expectedTableLines = Object.keys(rules)
12+
.sort()
13+
.reduce(
14+
(lines, ruleId) => {
15+
const rule = rules[ruleId];
16+
17+
lines.push(
18+
[
19+
`[${ruleId}](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/${ruleId}.md)`,
20+
rule.meta.docs.recommended ? '✔️' : '',
21+
rule.meta.fixable ? '🛠' : '',
22+
rule.meta.docs.description,
23+
].join(' | ')
24+
);
25+
26+
return lines;
27+
},
28+
['Name | ✔️ | 🛠 | Description', '----- | ----- | ----- | -----']
29+
)
30+
.join('\n');
31+
32+
const readmeContents = fs.readFileSync(README_LOCATION, 'utf8');
33+
34+
if (!readmeContents.includes(BEGIN_TABLE_MARKER)) {
35+
throw new Error(
36+
`Could not find '${BEGIN_TABLE_MARKER}' marker in README.md.`
37+
);
38+
}
39+
40+
if (!readmeContents.includes(END_TABLE_MARKER)) {
41+
throw new Error(`Could not find '${END_TABLE_MARKER}' marker in README.md.`);
42+
}
43+
44+
const linesStartIndex =
45+
readmeContents.indexOf(BEGIN_TABLE_MARKER) + BEGIN_TABLE_MARKER.length;
46+
const linesEndIndex = readmeContents.indexOf(END_TABLE_MARKER);
47+
48+
const updatedReadmeContents =
49+
readmeContents.slice(0, linesStartIndex) +
50+
expectedTableLines +
51+
readmeContents.slice(linesEndIndex);
52+
53+
if (module.parent) {
54+
module.exports = updatedReadmeContents;
55+
} else {
56+
fs.writeFileSync(README_LOCATION, updatedReadmeContents);
57+
}

docs/rules/prefer-enabled-disabled.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# prefer toBeDisabled() or toBeEnabled() over toHaveProperty('disabled', true|false) (prefer-enabled-disabled)
2+
3+
## Rule Details
4+
5+
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.
6+
7+
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.
8+
9+
### False positives
10+
11+
Consider these 2 snippets:
12+
13+
```js
14+
const { getByRole } = render(<input type="checkbox" disabled />);
15+
const element = getByRole('checkbox');
16+
expect(element).toHaveProperty('disabled'); // passes
17+
18+
const { getByRole } = render(<input type="checkbox" />);
19+
const element = getByRole('checkbox');
20+
expect(element).toHaveProperty('disabled'); // also passes 😱
21+
```
22+
23+
### Readability
24+
25+
Consider the following snippets:
26+
27+
```js
28+
const { getByRole } = render(<input type="checkbox" />);
29+
const element = getByRole('checkbox');
30+
31+
expect(element).toHaveAttribute('disabled', false); // fails
32+
expect(element).toHaveAttribute('disabled', ''); // fails
33+
expect(element).not.toHaveAttribute('disabled', ''); // passes
34+
35+
expect(element).not.toHaveAttribute('disabled', true); // passes.
36+
expect(element).not.toHaveAttribute('disabled', false); // also passes.
37+
```
38+
39+
As you can see, using `toHaveAttribute` in this case is confusing, unintuitive and can even lead to false positive tests.
40+
41+
Examples of **incorrect** code for this rule:
42+
43+
```js
44+
expect(element).toHaveProperty('disabled', true);
45+
expect(element).toHaveAttribute('disabled', false);
46+
47+
expect(element).toHaveAttribute('disabled');
48+
expect(element).not.toHaveProperty('disabled');
49+
50+
expect(element).not.toBeDisabled();
51+
expect(element).not.toBeEnabled();
52+
```
53+
54+
Examples of **correct** code for this rule:
55+
56+
```js
57+
expect(element).toBeEnabled();
58+
59+
expect(element).toBeDisabled();
60+
61+
expect(element).toHaveProperty('checked', true);
62+
63+
expect(element).toHaveAttribute('checked');
64+
```
65+
66+
## When Not To Use It
67+
68+
Don't use this rule if you:
69+
70+
- don't use `jest-dom`
71+
- want to allow `.toHaveProperty('disabled', true|false);`
72+
73+
## Further reading
74+
75+
- [toBeDisabled](https://github.com/testing-library/jest-dom#tobedisabled)
76+
- [toBeEnabled](https://github.com/testing-library/jest-dom#tobeenabled)

jest.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
testMatch: ['**/tests/**/*.js'],
3+
collectCoverage: true,
4+
coverageThreshold: {
5+
global: {
6+
branches: 96.55,
7+
functions: 100,
8+
lines: 98.97,
9+
statements: 0,
10+
},
11+
},
12+
testPathIgnorePatterns: ['<rootDir>/tests/fixtures/'],
13+
collectCoverageFrom: ['lib/**/*.js', '!**/node_modules/**'],
14+
};

lib/createBannedAttributeRule.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
module.exports = ({ preferred, negatedPreferred, attributes }) => context => {
2+
function getCorrectFunctionFor(node, negated = false) {
3+
return (node.arguments.length === 1 ||
4+
node.arguments[1].value === true ||
5+
node.arguments[1].value === '') &&
6+
!negated
7+
? preferred
8+
: negatedPreferred;
9+
}
10+
11+
const isBannedArg = node =>
12+
attributes.some(attr => attr === node.arguments[0].value);
13+
14+
return {
15+
[`CallExpression[callee.property.name=/${preferred}|${negatedPreferred}/][callee.object.property.name='not'][callee.object.object.callee.name='expect']`](
16+
node
17+
) {
18+
if (negatedPreferred.startsWith('toBe')) {
19+
const incorrectFunction = node.callee.property.name;
20+
21+
const correctFunction =
22+
incorrectFunction === preferred ? negatedPreferred : preferred;
23+
context.report({
24+
message: `Use ${correctFunction}() instead of not.${incorrectFunction}()`,
25+
node,
26+
fix(fixer) {
27+
return fixer.replaceTextRange(
28+
[node.callee.object.property.start, node.end],
29+
`${correctFunction}()`
30+
);
31+
},
32+
});
33+
}
34+
},
35+
"CallExpression[callee.property.name=/toHaveProperty|toHaveAttribute/][callee.object.property.name='not'][callee.object.object.callee.name='expect']"(
36+
node
37+
) {
38+
const arg = node.arguments[0].value;
39+
if (isBannedArg(node)) {
40+
const correctFunction = getCorrectFunctionFor(node, true);
41+
42+
const incorrectFunction = node.callee.property.name;
43+
context.report({
44+
message: `Use ${correctFunction}() instead of not.${incorrectFunction}('${arg}')`,
45+
node,
46+
fix(fixer) {
47+
return fixer.replaceTextRange(
48+
[node.callee.object.property.start, node.end],
49+
`${correctFunction}()`
50+
);
51+
},
52+
});
53+
}
54+
},
55+
"CallExpression[callee.object.callee.name='expect'][callee.property.name=/toHaveProperty|toHaveAttribute/]"(
56+
node
57+
) {
58+
if (isBannedArg(node)) {
59+
const correctFunction = getCorrectFunctionFor(node);
60+
61+
const incorrectFunction = node.callee.property.name;
62+
63+
const message = `Use ${correctFunction}() instead of ${incorrectFunction}(${node.arguments
64+
.map(({ raw }) => raw)
65+
.join(', ')})`;
66+
context.report({
67+
node: node.callee.property,
68+
message,
69+
fix(fixer) {
70+
return [
71+
fixer.replaceTextRange(
72+
[node.callee.property.start, node.end],
73+
`${correctFunction}()`
74+
),
75+
];
76+
},
77+
});
78+
}
79+
},
80+
};
81+
};

lib/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @fileoverview lint rules for use with jest-dom
3+
* @author Ben Monro
4+
*/
5+
'use strict';
6+
7+
//------------------------------------------------------------------------------
8+
// Requirements
9+
//------------------------------------------------------------------------------
10+
11+
let requireIndex = require('requireindex');
12+
13+
//------------------------------------------------------------------------------
14+
// Plugin Definition
15+
//------------------------------------------------------------------------------
16+
17+
// import all rules in lib/rules
18+
module.exports.rules = requireIndex(__dirname + '/rules');

lib/rules/prefer-checked.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
3+
* @author Ben Monro
4+
*/
5+
'use strict';
6+
7+
const createBannedAttributeRule = require('../createBannedAttributeRule');
8+
9+
module.exports = {
10+
meta: {
11+
docs: {
12+
description:
13+
'prefer toBeChecked over checking attributes',
14+
category: 'jest-dom',
15+
recommended: true,
16+
url: 'prefer-checked',
17+
},
18+
fixable: 'code',
19+
},
20+
21+
create: createBannedAttributeRule({
22+
preferred: 'toBeChecked',
23+
negatedPreferred: 'not.toBeChecked',
24+
attributes: ['checked', 'aria-checked'],
25+
}),
26+
};

0 commit comments

Comments
 (0)