Skip to content

Commit 18b4258

Browse files
authored
feat: new rule prefer-to-have-attribute (#17)
* added prefer-to-have-attribute * null checking * toMatch & toContain * more use cases * lint * updated readme
1 parent ec23b96 commit 18b4258

File tree

5 files changed

+420
-7
lines changed

5 files changed

+420
-7
lines changed

README.md

+6-7
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,12 @@ To enable this configuration use the `extends` property in your
6060
🛠 indicates that a rule is fixable.
6161

6262
<!-- __BEGIN AUTOGENERATED TABLE__ -->
63-
64-
| Name | ✔️ | 🛠 | Description |
65-
| -------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | ----------------------------------------------------------- |
66-
| [prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | ✔️ | 🛠 | prefer toBeChecked over checking attributes |
67-
| [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 |
68-
| [prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | ✔️ | 🛠 | prefer toBeRequired over checking properties |
69-
63+
Name | ✔️ | 🛠 | Description
64+
----- | ----- | ----- | -----
65+
[prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | ✔️ | 🛠 | prefer toBeChecked over checking attributes
66+
[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
67+
[prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | ✔️ | 🛠 | prefer toBeRequired over checking properties
68+
[prefer-to-have-attribute](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-to-have-attribute.md) | | 🛠 | prefer toHaveAttribute over checking getAttribute/hasAttribute
7069
<!-- __END AUTOGENERATED TABLE__ -->
7170

7271
## Contributors ✨
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# prefer toHaveAttribute over checking getAttribute/hasAttribute (prefer-to-have-attribute)
2+
3+
This rule is an autofixable rule that reports usages of `getAttribute` or `hasAttribute` in expect statements in preference of using the jest-dom `toHaveAttribute` matcher.
4+
5+
6+
## Rule Details
7+
8+
This checks the various built in jest-dom matchers when used in conjunction with get/hasAttribute. The only valid use case if when using greater/less than matchers since there isn't any equivalent use with `toHaveAttribute()`
9+
10+
Examples of **incorrect** code for this rule:
11+
12+
```js
13+
14+
expect(element.getAttribute('foo')).toMatch(/bar/)
15+
expect(element.getAttribute('foo')).toContain('bar')
16+
expect(getByText('thing').getAttribute("foo")).toBe("bar")
17+
expect(getByText("yes").getAttribute("data-blah")).toBe(expect.stringMatching(/foo/))
18+
expect(element.hasAttribute("foo")).toBeTruthy()
19+
20+
21+
```
22+
23+
Examples of **correct** code for this rule:
24+
25+
```js
26+
27+
expect(element.foo).toBeTruthy()
28+
expect(element.getAttributeNode()).toBeNull()
29+
expect(element.getAttribute('foo')).toBeGreaterThan(2)
30+
expect(element.getAttribute('foo')).toBeLessThan(2)
31+
32+
```
33+
34+
## When Not To Use It
35+
36+
If you don't care about using built in matchers for checking attributes on dom elements.
37+
38+
## Further Reading
39+
40+
* [jest-dom toHaveAttribute](https://github.com/testing-library/jest-dom#tohaveattribute)
41+
* [getAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute)
42+
* [hasAttribute](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)

lib/rules/prefer-to-have-attribute.js

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* @fileoverview prefer toHaveAttribute over checking getAttribute/hasAttribute
3+
* @author Ben Monro
4+
*/
5+
'use strict';
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
module.exports = {
12+
meta: {
13+
docs: {
14+
description:
15+
'prefer toHaveAttribute over checking getAttribute/hasAttribute ',
16+
recommended: false,
17+
},
18+
fixable: 'code',
19+
},
20+
21+
create: function(context) {
22+
return {
23+
[`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeNull/]`](
24+
node
25+
) {
26+
context.report({
27+
node: node.parent,
28+
message: `Use toHaveAttribute instead of asserting on getAttribute`,
29+
fix(fixer) {
30+
return [
31+
fixer.removeRange([node.callee.object.end, node.end]),
32+
fixer.replaceTextRange(
33+
[
34+
node.parent.parent.property.start,
35+
node.parent.parent.parent.end,
36+
],
37+
`not.toHaveAttribute(${node.arguments[0].raw})`
38+
),
39+
];
40+
},
41+
});
42+
},
43+
[`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toContain$|toMatch$/]`](
44+
node
45+
) {
46+
context.report({
47+
node: node.parent,
48+
message: `Use toHaveAttribute instead of asserting on getAttribute`,
49+
fix(fixer) {
50+
return [
51+
fixer.removeRange([node.callee.object.end, node.end]),
52+
fixer.replaceText(node.parent.parent.property, 'toHaveAttribute'),
53+
fixer.replaceText(
54+
node.parent.parent.parent.arguments[0],
55+
`${
56+
node.arguments[0].raw
57+
}, expect.string${node.parent.parent.property.name.slice(
58+
2
59+
)}ing(${node.parent.parent.parent.arguments[0].raw})`
60+
),
61+
];
62+
},
63+
});
64+
},
65+
[`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
66+
node
67+
) {
68+
const arg = node.parent.parent.parent.arguments;
69+
const isNullOrEmpty =
70+
arg.length > 0 && (arg[0].value === null || arg[0].value === '');
71+
72+
context.report({
73+
node: node.parent,
74+
message: `Use toHaveAttribute instead of asserting on getAttribute`,
75+
fix(fixer) {
76+
let lastFixer;
77+
if (isNullOrEmpty) {
78+
lastFixer = fixer.replaceText(
79+
node.parent.parent.parent.arguments[0],
80+
node.arguments[0].raw
81+
);
82+
} else {
83+
lastFixer = fixer.insertTextBefore(
84+
node.parent.parent.parent.arguments[0],
85+
`${node.arguments[0].raw}, `
86+
);
87+
}
88+
return [
89+
fixer.removeRange([node.callee.object.end, node.end]),
90+
fixer.replaceText(
91+
node.parent.parent.property,
92+
`${isNullOrEmpty ? 'not.' : ''}toHaveAttribute`
93+
),
94+
lastFixer,
95+
];
96+
},
97+
});
98+
},
99+
[`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeNull|toBeUndefined|toBeDefined/]`](
100+
node
101+
) {
102+
context.report({
103+
node: node.parent.parent.property,
104+
message: 'Invalid matcher for hasAttribute',
105+
});
106+
},
107+
[`CallExpression[callee.property.name='getAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeUndefined|toBeDefined/]`](
108+
node
109+
) {
110+
context.report({
111+
node: node.parent.parent.property,
112+
message: 'Invalid matcher for getAttribute',
113+
});
114+
},
115+
[`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Striclty)?Equal/]`](
116+
node
117+
) {
118+
if (typeof node.parent.parent.parent.arguments[0].value === 'boolean') {
119+
context.report({
120+
node: node.parent,
121+
message: `Use toHaveAttribute instead of asserting on hasAttribute`,
122+
fix(fixer) {
123+
return [
124+
fixer.removeRange([node.callee.object.end, node.end]),
125+
fixer.replaceText(
126+
node.parent.parent.property,
127+
`${
128+
node.parent.parent.parent.arguments[0].value === false
129+
? 'not.'
130+
: ''
131+
}toHaveAttribute`
132+
),
133+
fixer.replaceText(
134+
node.parent.parent.parent.arguments[0],
135+
node.arguments[0].raw
136+
),
137+
];
138+
},
139+
});
140+
} else {
141+
context.report({
142+
node: node.parent.parent.property,
143+
message: 'Invalid matcher for hasAttribute',
144+
});
145+
}
146+
},
147+
[`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBeTruthy|toBeFalsy/]`](
148+
node
149+
) {
150+
context.report({
151+
node: node.parent,
152+
message: `Use toHaveAttribute instead of asserting on hasAttribute`,
153+
fix(fixer) {
154+
return [
155+
fixer.removeRange([node.callee.object.end, node.end]),
156+
fixer.replaceTextRange(
157+
[
158+
node.parent.parent.property.start,
159+
node.parent.parent.parent.end,
160+
],
161+
`${
162+
node.parent.parent.property.name === 'toBeFalsy' ? 'not.' : ''
163+
}toHaveAttribute(${node.arguments[0].raw})`
164+
),
165+
];
166+
},
167+
});
168+
},
169+
};
170+
},
171+
};

tests/__snapshots__/index.test.js.snap

+10
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,15 @@ Object {
3838
"fixable": "code",
3939
},
4040
},
41+
"prefer-to-have-attribute": Object {
42+
"create": [Function],
43+
"meta": Object {
44+
"docs": Object {
45+
"description": "prefer toHaveAttribute over checking getAttribute/hasAttribute ",
46+
"recommended": false,
47+
},
48+
"fixable": "code",
49+
},
50+
},
4151
}
4252
`;

0 commit comments

Comments
 (0)