Skip to content

Commit 8a55022

Browse files
authored
feat(rules): added new rule for prefer-to-have-style (#76)
* feat(rules): added new rule prefer-to-have-style * updated readme * updated readme * updated readme * updated readme fix(rules): use .range instead of start/end
1 parent e3a12dc commit 8a55022

14 files changed

+377
-117
lines changed

README.md

+12-10
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,22 @@ module.exports = {
9595

9696
## Supported Rules
9797

98-
✔️ indicates that a rule is recommended for all users.
98+
👍 indicates that a rule is recommended for all users.
9999

100-
🛠 indicates that a rule is fixable.
100+
🔧 indicates that a rule is fixable.
101101

102102
<!-- __BEGIN AUTOGENERATED TABLE__ -->
103103

104-
| Name | ✔️ | 🛠 | Description |
104+
| Name | 👍 | 🔧 | Description |
105105
| ---------------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | -------------------------------------------------------------- |
106-
| [prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | ✔️ | 🛠 | prefer toBeChecked over checking attributes |
107-
| [prefer-empty](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-empty.md) | ✔️ | 🛠 | Prefer toBeEmpty over checking innerHTML |
108-
| [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 |
109-
| [prefer-focus](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-focus.md) | ✔️ | 🛠 | prefer toHaveFocus over checking document.activeElement |
110-
| [prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | ✔️ | 🛠 | prefer toBeRequired over checking properties |
111-
| [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 |
112-
| [prefer-to-have-text-content](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-to-have-text-content.md) | ✔️ | 🛠 | Prefer toHaveTextContent over checking element.textContent |
106+
| [prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | 👍 | 🔧 | prefer toBeChecked over checking attributes |
107+
| [prefer-empty](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-empty.md) | 👍 | 🔧 | Prefer toBeEmpty over checking innerHTML |
108+
| [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 |
109+
| [prefer-focus](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-focus.md) | 👍 | 🔧 | prefer toHaveFocus over checking document.activeElement |
110+
| [prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | 👍 | 🔧 | prefer toBeRequired over checking properties |
111+
| [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 |
112+
| [prefer-to-have-style](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-to-have-style.md) | 👍 | 🔧 | prefer toHaveStyle over checking element style |
113+
| [prefer-to-have-text-content](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-to-have-text-content.md) | 👍 | 🔧 | Prefer toHaveTextContent over checking element.textContent |
113114

114115
<!-- __END AUTOGENERATED TABLE__ -->
115116

@@ -156,6 +157,7 @@ Thanks goes to these people ([emoji key][emojis]):
156157

157158
<!-- markdownlint-enable -->
158159
<!-- prettier-ignore-end -->
160+
159161
<!-- ALL-CONTRIBUTORS-LIST:END -->
160162

161163
This project follows the [all-contributors][all-contributors] specification.

build/generate-readme-table.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ const expectedTableLines = Object.keys(rules)
1515
lines.push(
1616
[
1717
`[${ruleId}](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/${ruleId}.md)`,
18-
rule.meta.docs.recommended ? "✔️" : "",
19-
rule.meta.fixable ? "🛠" : "",
18+
rule.meta.docs.recommended ? "👍" : "",
19+
rule.meta.fixable ? "🔧" : "",
2020
rule.meta.docs.description,
2121
].join(" | ")
2222
);
2323

2424
return lines;
2525
},
26-
["Name | ✔️ | 🛠 | Description", "----- | ----- | ----- | -----"]
26+
["Name | 👍 | 🔧 | Description", "----- | ----- | ----- | -----"]
2727
)
2828
.join("\n");
2929

docs/rules/prefer-to-have-style.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# prefer toHaveProperty over checking element.style (prefer-to-have-style)
2+
3+
This rule is an autofixable rule that reports usages of checking element.style in expect statements in preference of using the jest-dom
4+
`toHaveStyle` matcher.
5+
6+
## Rule Details
7+
8+
Examples of **incorrect** code for this rule:
9+
10+
```js
11+
expect(el.style.foo).toBe("bar");
12+
expect(el.style.foo).not.toBe("bar");
13+
expect(el.style).toHaveProperty("background-color", "green");
14+
expect(screen.getByTestId("foo").style["scroll-snap-type"]).toBe("x mandatory");
15+
expect(el.style).toContain("background-color");
16+
expect(el.style).not.toContain("background-color");
17+
expect(el).toHaveAttribute(
18+
"style",
19+
"background-color: green; border-width: 10px; color: blue;"
20+
);
21+
```
22+
23+
Examples of **correct** code for this rule:
24+
25+
```js
26+
expect(el).toHaveStyle({ foo: "bar" });
27+
expect(el.style).toMatchSnapshot();
28+
expect(el.style).toEqual(foo);
29+
```
30+
31+
## When Not To Use It
32+
33+
If you don't care about using built in matchers for checking style on dom
34+
elements.
35+
36+
## Further Reading
37+
38+
- [jest-dom toHaveStyle](https://github.com/testing-library/jest-dom#tohavestyle)
39+
- [ElementCSSInlineStyle.style](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style)

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"requireindex": "^1.2.0"
3636
},
3737
"devDependencies": {
38+
"jest-extended": "^0.11.5",
3839
"kcd-scripts": "^6.0.0"
3940
},
4041
"peerDependencies": {

src/__tests__/__snapshots__/index.test.js.snap

-85
This file was deleted.

src/__tests__/index.test.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import { rules, generateRecommendedConfig } from "../index";
2+
import "jest-extended";
23

34
it("should have all the rules", () => {
4-
expect(rules).toMatchSnapshot();
5+
expect(Object.keys(rules).length).toBeGreaterThan(0);
6+
});
7+
8+
it.each(Object.keys(rules))("%s should export required fields", (ruleName) => {
9+
const rule = rules[ruleName];
10+
expect(rule).toHaveProperty("create", expect.any(Function));
11+
expect(rule.meta.docs.url).not.toBeEmpty();
12+
expect(rule.meta.docs.category).toBe("jest-dom");
13+
expect(rule.meta.docs.description).not.toBeEmpty();
514
});
615

716
it("should have a recommended config with recommended rules", () => {

src/__tests__/lib/rules/.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{"rules":{
2+
"no-template-curly-in-string":"off"
3+
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { RuleTester } from "eslint";
2+
import * as rule from "../../../rules/prefer-to-have-style";
3+
4+
const errors = [
5+
{ message: "Use toHaveStyle instead of asserting on element style" },
6+
];
7+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
8+
ruleTester.run("prefer-to-have-attribute", rule, {
9+
valid: [
10+
`expect(el).toHaveStyle({foo:"bar"})`,
11+
`expect(el.style).toMatchSnapshot()`,
12+
`expect(el.style).toEqual(foo)`,
13+
`expect(el).toHaveAttribute("style")`,
14+
],
15+
invalid: [
16+
{
17+
code: `expect(el.style.foo).toBe("bar")`,
18+
errors,
19+
output: `expect(el).toHaveStyle({foo:"bar"})`,
20+
},
21+
{
22+
code: `expect(el.style.foo).not.toBe("bar")`,
23+
errors,
24+
output: `expect(el).not.toHaveStyle({foo:"bar"})`,
25+
},
26+
{
27+
code: `expect(el.style).toHaveProperty("background-color", "green")`,
28+
errors,
29+
output: `expect(el).toHaveStyle({backgroundColor: "green"})`,
30+
},
31+
{
32+
code: `expect(el.style).not.toHaveProperty("background-color", "green")`,
33+
errors,
34+
output: `expect(el).not.toHaveStyle({backgroundColor: "green"})`,
35+
},
36+
{
37+
code: `expect(screen.getByTestId("foo").style["scroll-snap-type"]).toBe("x mandatory")`,
38+
errors,
39+
output: `expect(screen.getByTestId("foo")).toHaveStyle({scrollSnapType: "x mandatory"})`,
40+
},
41+
{
42+
code: `expect(screen.getByTestId("foo").style["scroll-snap-type"]).not.toBe("x mandatory")`,
43+
errors,
44+
output: `expect(screen.getByTestId("foo")).not.toHaveStyle({scrollSnapType: "x mandatory"})`,
45+
},
46+
{
47+
code: `expect(el.style).toContain("background-color")`,
48+
errors,
49+
output: `expect(el).toHaveStyle({backgroundColor: expect.anything()})`,
50+
},
51+
{
52+
code: `expect(el.style).not.toContain("background-color")`,
53+
errors,
54+
output: `expect(el).not.toHaveStyle({backgroundColor: expect.anything()})`,
55+
},
56+
{
57+
code: `expect(el).toHaveAttribute("style", "background-color: green; border-width: 10px; color: blue;")`,
58+
errors,
59+
output: `expect(el).toHaveStyle("background-color: green; border-width: 10px; color: blue;")`,
60+
},
61+
],
62+
});

src/createBannedAttributeRule.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default ({ preferred, negatedPreferred, attributes }) => (context) => {
3939
node
4040
) {
4141
const {
42-
arguments: [{ property, property: { name } = {} }],
42+
arguments: [{ object, property, property: { name } = {} }],
4343
} = node.callee.object;
4444
const matcher = node.callee.property.name;
4545
const matcherArg = node.arguments.length && node.arguments[0].value;
@@ -58,7 +58,7 @@ export default ({ preferred, negatedPreferred, attributes }) => (context) => {
5858
node,
5959
message: `Use ${correctFunction}() instead of checking .${name} directly`,
6060
fix: (fixer) => [
61-
fixer.removeRange([property.range[0] - 1, property.range[1]]),
61+
fixer.removeRange([object.range[1], property.range[1]]),
6262
fixer.replaceTextRange(
6363
[node.callee.property.range[0], node.range[1]],
6464
`${correctFunction}()`

src/rules/prefer-empty.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const create = (context) => ({
2121
node,
2222
message: "Use toBeEmptyDOMElement instead of checking inner html.",
2323
fix: (fixer) => [
24-
fixer.removeRange([node.left.property.range[0] - 1, node.range[1]]),
24+
fixer.removeRange([node.left.object.range[1], node.range[1]]),
2525
fixer.replaceText(
2626
node.parent.parent.property,
2727
Boolean(node.parent.parent.parent.arguments[0].value) ===
@@ -40,7 +40,7 @@ export const create = (context) => ({
4040
node,
4141
message: "Use toBeEmptyDOMElement instead of checking inner html.",
4242
fix: (fixer) => [
43-
fixer.removeRange([node.left.property.range[0] - 1, node.range[1]]),
43+
fixer.removeRange([node.left.object.range[1], node.range[1]]),
4444
fixer.replaceText(
4545
node.parent.parent.property,
4646
Boolean(node.parent.parent.parent.arguments[0].value) ===
@@ -64,7 +64,7 @@ export const create = (context) => ({
6464
node,
6565
message: "Use toBeEmptyDOMElement instead of checking inner html.",
6666
fix: (fixer) => [
67-
fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
67+
fixer.removeRange([node.object.range[1], node.property.range[1]]),
6868
fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"),
6969
fixer.remove(node.parent.parent.parent.arguments[0]),
7070
],
@@ -83,7 +83,7 @@ export const create = (context) => ({
8383
node,
8484
message: "Use toBeEmptyDOMElement instead of checking inner html.",
8585
fix: (fixer) => [
86-
fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
86+
fixer.removeRange([node.object.range[1], node.property.range[1]]),
8787
fixer.replaceText(
8888
node.parent.parent.parent.property,
8989
"toBeEmptyDOMElement"
@@ -99,7 +99,7 @@ export const create = (context) => ({
9999
node,
100100
message: "Use toBeEmptyDOMElement instead of checking inner html.",
101101
fix: (fixer) => [
102-
fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
102+
fixer.removeRange([node.object.range[1], node.property.range[1]]),
103103
fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"),
104104
],
105105
});
@@ -115,7 +115,7 @@ export const create = (context) => ({
115115
node,
116116
message: "Use toBeEmptyDOMElement instead of checking inner html.",
117117
fix: (fixer) => [
118-
fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
118+
fixer.removeRange([node.object.range[1], node.property.range[1]]),
119119
fixer.replaceText(
120120
node.parent.parent.parent.property,
121121
"toBeEmptyDOMElement"
@@ -131,7 +131,7 @@ export const create = (context) => ({
131131
node,
132132
message: "Use toBeEmptyDOMElement instead of checking inner html.",
133133
fix: (fixer) => [
134-
fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
134+
fixer.removeRange([node.object.range[1], node.property.range[1]]),
135135
fixer.replaceText(
136136
node.parent.parent.parent.property,
137137
"toBeEmptyDOMElement"
@@ -150,7 +150,7 @@ export const create = (context) => ({
150150
node,
151151
message: "Use toBeEmptyDOMElement instead of checking inner html.",
152152
fix: (fixer) => [
153-
fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]),
153+
fixer.removeRange([node.object.range[1], node.property.range[1]]),
154154
fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"),
155155
fixer.remove(node.parent.parent.parent.arguments[0]),
156156
],

src/rules/prefer-focus.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const variantsOfDoc = [
1414

1515
export const meta = {
1616
docs: {
17+
url: "prefer-focus",
1718
description: "prefer toHaveFocus over checking document.activeElement",
1819
category: "jest-dom",
1920
recommended: true,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
export const meta = {
1111
docs: {
12+
category: "jest-dom",
1213
description:
1314
"prefer toHaveAttribute over checking getAttribute/hasAttribute ",
15+
url: "prefer-to-have-attribute",
1416
recommended: true,
1517
},
1618
fixable: "code",
@@ -107,7 +109,7 @@ export const create = (context) => ({
107109
message: "Invalid matcher for getAttribute",
108110
});
109111
},
110-
[`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Striclty)?Equal/]`](
112+
[`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`](
111113
node
112114
) {
113115
if (typeof node.parent.parent.parent.arguments[0].value === "boolean") {

0 commit comments

Comments
 (0)