Skip to content

Commit

Permalink
feat: added 3 rules
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Monro committed Oct 27, 2019
0 parents commit 0823052
Show file tree
Hide file tree
Showing 18 changed files with 637 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage/
22 changes: 22 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"parserOptions": {
"ecmaVersion": 2018
},
"env": {
"commonjs": true,
"es6": true,
"node": true
// "jest/globals": true
},
"plugins": [
"eslint-plugin",
"prettier"
],
"extends": [
"plugin:eslint-plugin/recommended"
],
"rules": {
"prettier/prettier": "error",
"no-var": "error"
}
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
coverage
yarn.lock
package-lock.json
4 changes: 4 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"singleQuote": true
}
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# eslint-plugin-jest-dom

lint rules for use with jest-dom

## Installation

You'll first need to install [ESLint](http://eslint.org):

```
$ npm i eslint --save-dev
```

Next, install `eslint-plugin-jest-dom`:

```
$ npm install eslint-plugin-jest-dom --save-dev
```

**Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-jest-dom` globally.

## Usage

Add `jest-dom` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:

```json
{
"plugins": [
"jest-dom"
]
}
```


Then configure the rules you want to use under the rules section.

```json
{
"rules": {
"jest-dom/prefer-checked": "error",
"jest-dom/prefer-enabled-disabled": "error",
"jest-dom/prefer-required": "error"
}
}
```

## Supported Rules

✔️ indicates that a rule is recommended for all users.

🛠 indicates that a rule is fixable.

<!-- __BEGIN AUTOGENERATED TABLE__ -->
Name | ✔️ | 🛠 | Description
----- | ----- | ----- | -----
[prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | ✔️ | 🛠 | prefer toBeChecked over checking attributes
[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
[prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | ✔️ | 🛠 | prefer toBeRequired over checking properties
<!-- __END AUTOGENERATED TABLE__ -->





57 changes: 57 additions & 0 deletions build/generate-readme-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict';

const fs = require('fs');
const path = require('path');
const rules = require('..').rules;

const README_LOCATION = path.resolve(__dirname, '..', 'README.md');
const BEGIN_TABLE_MARKER = '<!-- __BEGIN AUTOGENERATED TABLE__ -->\n';
const END_TABLE_MARKER = '\n<!-- __END AUTOGENERATED TABLE__ -->';

const expectedTableLines = Object.keys(rules)
.sort()
.reduce(
(lines, ruleId) => {
const rule = rules[ruleId];

lines.push(
[
`[${ruleId}](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/${ruleId}.md)`,
rule.meta.docs.recommended ? '✔️' : '',
rule.meta.fixable ? '🛠' : '',
rule.meta.docs.description,
].join(' | ')
);

return lines;
},
['Name | ✔️ | 🛠 | Description', '----- | ----- | ----- | -----']
)
.join('\n');

const readmeContents = fs.readFileSync(README_LOCATION, 'utf8');

if (!readmeContents.includes(BEGIN_TABLE_MARKER)) {
throw new Error(
`Could not find '${BEGIN_TABLE_MARKER}' marker in README.md.`
);
}

if (!readmeContents.includes(END_TABLE_MARKER)) {
throw new Error(`Could not find '${END_TABLE_MARKER}' marker in README.md.`);
}

const linesStartIndex =
readmeContents.indexOf(BEGIN_TABLE_MARKER) + BEGIN_TABLE_MARKER.length;
const linesEndIndex = readmeContents.indexOf(END_TABLE_MARKER);

const updatedReadmeContents =
readmeContents.slice(0, linesStartIndex) +
expectedTableLines +
readmeContents.slice(linesEndIndex);

if (module.parent) {
module.exports = updatedReadmeContents;
} else {
fs.writeFileSync(README_LOCATION, updatedReadmeContents);
}
76 changes: 76 additions & 0 deletions docs/rules/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) (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)
14 changes: 14 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +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}()`
),
];
},
});
}
},
};
};
18 changes: 18 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @fileoverview lint rules for use with jest-dom
* @author Ben Monro
*/
'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

let requireIndex = require('requireindex');

//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------

// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + '/rules');
26 changes: 26 additions & 0 deletions lib/rules/prefer-checked.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @fileoverview prefer toBeDisabled or toBeEnabled over attribute checks
* @author Ben Monro
*/
'use strict';

const createBannedAttributeRule = require('../createBannedAttributeRule');

module.exports = {
meta: {
docs: {
description:
'prefer toBeChecked over checking attributes',
category: 'jest-dom',
recommended: true,
url: 'prefer-checked',
},
fixable: 'code',
},

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

0 comments on commit 0823052

Please sign in to comment.