From 8a550226eb4dda3a9ec68ee8640917554eff46b2 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Sun, 30 Aug 2020 20:09:09 -0700 Subject: [PATCH] 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 --- README.md | 22 +- build/generate-readme-table.js | 6 +- docs/rules/prefer-to-have-style.md | 39 +++ package.json | 1 + .../__snapshots__/index.test.js.snap | 85 ------- src/__tests__/index.test.js | 11 +- src/__tests__/lib/rules/.eslintrc | 3 + .../lib/rules/prefer-to-have-style.js | 62 +++++ src/createBannedAttributeRule.js | 4 +- src/rules/prefer-empty.js | 16 +- src/rules/prefer-focus.js | 1 + src/rules/prefer-to-have-attribute.js | 4 +- src/rules/prefer-to-have-style.js | 227 ++++++++++++++++++ src/rules/prefer-to-have-text-content.js | 13 +- 14 files changed, 377 insertions(+), 117 deletions(-) create mode 100644 docs/rules/prefer-to-have-style.md delete mode 100644 src/__tests__/__snapshots__/index.test.js.snap create mode 100644 src/__tests__/lib/rules/.eslintrc create mode 100644 src/__tests__/lib/rules/prefer-to-have-style.js create mode 100644 src/rules/prefer-to-have-style.js diff --git a/README.md b/README.md index fc3e121..5f92ef0 100644 --- a/README.md +++ b/README.md @@ -95,21 +95,22 @@ module.exports = { ## Supported Rules -✔️ indicates that a rule is recommended for all users. +👍 indicates that a rule is recommended for all users. -🛠 indicates that a rule is fixable. +🔧 indicates that a rule is fixable. -| Name | ✔️ | 🛠 | Description | +| 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-empty](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-empty.md) | ✔️ | 🛠 | Prefer toBeEmpty over checking innerHTML | -| [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-focus](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-focus.md) | ✔️ | 🛠 | prefer toHaveFocus over checking document.activeElement | -| [prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | ✔️ | 🛠 | prefer toBeRequired over checking properties | -| [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 | -| [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 | +| [prefer-checked](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-checked.md) | 👍 | 🔧 | prefer toBeChecked over checking attributes | +| [prefer-empty](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-empty.md) | 👍 | 🔧 | Prefer toBeEmpty over checking innerHTML | +| [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-focus](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-focus.md) | 👍 | 🔧 | prefer toHaveFocus over checking document.activeElement | +| [prefer-required](https://github.com/testing-library/eslint-plugin-jest-dom/blob/master/docs/rules/prefer-required.md) | 👍 | 🔧 | prefer toBeRequired over checking properties | +| [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 | +| [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 | +| [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 | @@ -156,6 +157,7 @@ Thanks goes to these people ([emoji key][emojis]): + This project follows the [all-contributors][all-contributors] specification. diff --git a/build/generate-readme-table.js b/build/generate-readme-table.js index c37561f..8e5cd22 100644 --- a/build/generate-readme-table.js +++ b/build/generate-readme-table.js @@ -15,15 +15,15 @@ const expectedTableLines = Object.keys(rules) 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.recommended ? "👍" : "", + rule.meta.fixable ? "🔧" : "", rule.meta.docs.description, ].join(" | ") ); return lines; }, - ["Name | ✔️ | 🛠 | Description", "----- | ----- | ----- | -----"] + ["Name | 👍 | 🔧 | Description", "----- | ----- | ----- | -----"] ) .join("\n"); diff --git a/docs/rules/prefer-to-have-style.md b/docs/rules/prefer-to-have-style.md new file mode 100644 index 0000000..7226845 --- /dev/null +++ b/docs/rules/prefer-to-have-style.md @@ -0,0 +1,39 @@ +# prefer toHaveProperty over checking element.style (prefer-to-have-style) + +This rule is an autofixable rule that reports usages of checking element.style in expect statements in preference of using the jest-dom +`toHaveStyle` matcher. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```js +expect(el.style.foo).toBe("bar"); +expect(el.style.foo).not.toBe("bar"); +expect(el.style).toHaveProperty("background-color", "green"); +expect(screen.getByTestId("foo").style["scroll-snap-type"]).toBe("x mandatory"); +expect(el.style).toContain("background-color"); +expect(el.style).not.toContain("background-color"); +expect(el).toHaveAttribute( + "style", + "background-color: green; border-width: 10px; color: blue;" +); +``` + +Examples of **correct** code for this rule: + +```js +expect(el).toHaveStyle({ foo: "bar" }); +expect(el.style).toMatchSnapshot(); +expect(el.style).toEqual(foo); +``` + +## When Not To Use It + +If you don't care about using built in matchers for checking style on dom +elements. + +## Further Reading + +- [jest-dom toHaveStyle](https://github.com/testing-library/jest-dom#tohavestyle) +- [ElementCSSInlineStyle.style](https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style) diff --git a/package.json b/package.json index 7dca7af..4a094c8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "requireindex": "^1.2.0" }, "devDependencies": { + "jest-extended": "^0.11.5", "kcd-scripts": "^6.0.0" }, "peerDependencies": { diff --git a/src/__tests__/__snapshots__/index.test.js.snap b/src/__tests__/__snapshots__/index.test.js.snap deleted file mode 100644 index 81200c9..0000000 --- a/src/__tests__/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,85 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should have all the rules 1`] = ` -Object { - "prefer-checked": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "category": "jest-dom", - "description": "prefer toBeChecked over checking attributes", - "recommended": true, - "url": "prefer-checked", - }, - "fixable": "code", - }, - }, - "prefer-empty": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "category": "jest-dom", - "description": "Prefer toBeEmpty over checking innerHTML", - "recommended": true, - "url": "prefer-empty", - }, - "fixable": "code", - }, - }, - "prefer-enabled-disabled": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "category": "jest-dom", - "description": "prefer toBeDisabled or toBeEnabled over checking attributes", - "recommended": true, - "url": "prefer-enabled-disabled", - }, - "fixable": "code", - }, - }, - "prefer-focus": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "category": "jest-dom", - "description": "prefer toHaveFocus over checking document.activeElement", - "recommended": true, - }, - "fixable": "code", - }, - }, - "prefer-required": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "category": "jest-dom", - "description": "prefer toBeRequired over checking properties", - "recommended": true, - "url": "prefer-required", - }, - "fixable": "code", - }, - }, - "prefer-to-have-attribute": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "description": "prefer toHaveAttribute over checking getAttribute/hasAttribute ", - "recommended": true, - }, - "fixable": "code", - }, - }, - "prefer-to-have-text-content": Object { - "create": [Function], - "meta": Object { - "docs": Object { - "description": "Prefer toHaveTextContent over checking element.textContent", - "recommended": true, - }, - "fixable": "code", - }, - }, -} -`; diff --git a/src/__tests__/index.test.js b/src/__tests__/index.test.js index ad42abb..9e2fc57 100644 --- a/src/__tests__/index.test.js +++ b/src/__tests__/index.test.js @@ -1,7 +1,16 @@ import { rules, generateRecommendedConfig } from "../index"; +import "jest-extended"; it("should have all the rules", () => { - expect(rules).toMatchSnapshot(); + expect(Object.keys(rules).length).toBeGreaterThan(0); +}); + +it.each(Object.keys(rules))("%s should export required fields", (ruleName) => { + const rule = rules[ruleName]; + expect(rule).toHaveProperty("create", expect.any(Function)); + expect(rule.meta.docs.url).not.toBeEmpty(); + expect(rule.meta.docs.category).toBe("jest-dom"); + expect(rule.meta.docs.description).not.toBeEmpty(); }); it("should have a recommended config with recommended rules", () => { diff --git a/src/__tests__/lib/rules/.eslintrc b/src/__tests__/lib/rules/.eslintrc new file mode 100644 index 0000000..f18e3da --- /dev/null +++ b/src/__tests__/lib/rules/.eslintrc @@ -0,0 +1,3 @@ +{"rules":{ + "no-template-curly-in-string":"off" +}} diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js new file mode 100644 index 0000000..e800a30 --- /dev/null +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -0,0 +1,62 @@ +import { RuleTester } from "eslint"; +import * as rule from "../../../rules/prefer-to-have-style"; + +const errors = [ + { message: "Use toHaveStyle instead of asserting on element style" }, +]; +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } }); +ruleTester.run("prefer-to-have-attribute", rule, { + valid: [ + `expect(el).toHaveStyle({foo:"bar"})`, + `expect(el.style).toMatchSnapshot()`, + `expect(el.style).toEqual(foo)`, + `expect(el).toHaveAttribute("style")`, + ], + invalid: [ + { + code: `expect(el.style.foo).toBe("bar")`, + errors, + output: `expect(el).toHaveStyle({foo:"bar"})`, + }, + { + code: `expect(el.style.foo).not.toBe("bar")`, + errors, + output: `expect(el).not.toHaveStyle({foo:"bar"})`, + }, + { + code: `expect(el.style).toHaveProperty("background-color", "green")`, + errors, + output: `expect(el).toHaveStyle({backgroundColor: "green"})`, + }, + { + code: `expect(el.style).not.toHaveProperty("background-color", "green")`, + errors, + output: `expect(el).not.toHaveStyle({backgroundColor: "green"})`, + }, + { + code: `expect(screen.getByTestId("foo").style["scroll-snap-type"]).toBe("x mandatory")`, + errors, + output: `expect(screen.getByTestId("foo")).toHaveStyle({scrollSnapType: "x mandatory"})`, + }, + { + code: `expect(screen.getByTestId("foo").style["scroll-snap-type"]).not.toBe("x mandatory")`, + errors, + output: `expect(screen.getByTestId("foo")).not.toHaveStyle({scrollSnapType: "x mandatory"})`, + }, + { + code: `expect(el.style).toContain("background-color")`, + errors, + output: `expect(el).toHaveStyle({backgroundColor: expect.anything()})`, + }, + { + code: `expect(el.style).not.toContain("background-color")`, + errors, + output: `expect(el).not.toHaveStyle({backgroundColor: expect.anything()})`, + }, + { + code: `expect(el).toHaveAttribute("style", "background-color: green; border-width: 10px; color: blue;")`, + errors, + output: `expect(el).toHaveStyle("background-color: green; border-width: 10px; color: blue;")`, + }, + ], +}); diff --git a/src/createBannedAttributeRule.js b/src/createBannedAttributeRule.js index 513fdb0..519697b 100644 --- a/src/createBannedAttributeRule.js +++ b/src/createBannedAttributeRule.js @@ -39,7 +39,7 @@ export default ({ preferred, negatedPreferred, attributes }) => (context) => { node ) { const { - arguments: [{ property, property: { name } = {} }], + arguments: [{ object, property, property: { name } = {} }], } = node.callee.object; const matcher = node.callee.property.name; const matcherArg = node.arguments.length && node.arguments[0].value; @@ -58,7 +58,7 @@ export default ({ preferred, negatedPreferred, attributes }) => (context) => { node, message: `Use ${correctFunction}() instead of checking .${name} directly`, fix: (fixer) => [ - fixer.removeRange([property.range[0] - 1, property.range[1]]), + fixer.removeRange([object.range[1], property.range[1]]), fixer.replaceTextRange( [node.callee.property.range[0], node.range[1]], `${correctFunction}()` diff --git a/src/rules/prefer-empty.js b/src/rules/prefer-empty.js index 9262fd8..e9d2009 100644 --- a/src/rules/prefer-empty.js +++ b/src/rules/prefer-empty.js @@ -21,7 +21,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.left.property.range[0] - 1, node.range[1]]), + fixer.removeRange([node.left.object.range[1], node.range[1]]), fixer.replaceText( node.parent.parent.property, Boolean(node.parent.parent.parent.arguments[0].value) === @@ -40,7 +40,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.left.property.range[0] - 1, node.range[1]]), + fixer.removeRange([node.left.object.range[1], node.range[1]]), fixer.replaceText( node.parent.parent.property, Boolean(node.parent.parent.parent.arguments[0].value) === @@ -64,7 +64,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"), fixer.remove(node.parent.parent.parent.arguments[0]), ], @@ -83,7 +83,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText( node.parent.parent.parent.property, "toBeEmptyDOMElement" @@ -99,7 +99,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"), ], }); @@ -115,7 +115,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText( node.parent.parent.parent.property, "toBeEmptyDOMElement" @@ -131,7 +131,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText( node.parent.parent.parent.property, "toBeEmptyDOMElement" @@ -150,7 +150,7 @@ export const create = (context) => ({ node, message: "Use toBeEmptyDOMElement instead of checking inner html.", fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"), fixer.remove(node.parent.parent.parent.arguments[0]), ], diff --git a/src/rules/prefer-focus.js b/src/rules/prefer-focus.js index 59394bc..d0f0616 100644 --- a/src/rules/prefer-focus.js +++ b/src/rules/prefer-focus.js @@ -14,6 +14,7 @@ const variantsOfDoc = [ export const meta = { docs: { + url: "prefer-focus", description: "prefer toHaveFocus over checking document.activeElement", category: "jest-dom", recommended: true, diff --git a/src/rules/prefer-to-have-attribute.js b/src/rules/prefer-to-have-attribute.js index da7c512..4a08a49 100644 --- a/src/rules/prefer-to-have-attribute.js +++ b/src/rules/prefer-to-have-attribute.js @@ -9,8 +9,10 @@ export const meta = { docs: { + category: "jest-dom", description: "prefer toHaveAttribute over checking getAttribute/hasAttribute ", + url: "prefer-to-have-attribute", recommended: true, }, fixable: "code", @@ -107,7 +109,7 @@ export const create = (context) => ({ message: "Invalid matcher for getAttribute", }); }, - [`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Striclty)?Equal/]`]( + [`CallExpression[callee.property.name='hasAttribute'][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`]( node ) { if (typeof node.parent.parent.parent.arguments[0].value === "boolean") { diff --git a/src/rules/prefer-to-have-style.js b/src/rules/prefer-to-have-style.js new file mode 100644 index 0000000..4bc91db --- /dev/null +++ b/src/rules/prefer-to-have-style.js @@ -0,0 +1,227 @@ +/** + * @fileoverview prefer toHaveStyle over checking element style + * @author Ben Monro + */ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +const camelCase = (str) => str.replace(/-([a-z])/g, (c) => c[1].toUpperCase()); +export const meta = { + docs: { + category: "jest-dom", + url: "prefer-to-have-style", + description: "prefer toHaveStyle over checking element style", + recommended: true, + }, + fixable: "code", +}; + +export const create = (context) => ({ + //expect(el.style.foo).toBe("bar"); + [`MemberExpression[property.name=style][parent.computed=false][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.callee.name=expect]`]( + node + ) { + const styleName = node.parent.property; + const [styleValue] = node.parent.parent.parent.parent.arguments; + const matcher = node.parent.parent.parent.property; + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], styleName.range[1]]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceText( + styleValue, + `{${styleName.name}:${styleValue.raw}}` + ), + ]; + }, + }); + }, + //expect(el.style.foo).not.toBe("bar"); + [`MemberExpression[property.name=style][parent.computed=false][parent.parent.parent.property.name=not][parent.parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.callee.name=expect]`]( + node + ) { + const styleName = node.parent.property; + const styleValue = node.parent.parent.parent.parent.parent.arguments[0]; + const matcher = node.parent.parent.parent.parent.property; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], styleName.range[1]]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceText( + styleValue, + `{${styleName.name}:${styleValue.raw}}` + ), + ]; + }, + }); + }, + // expect(el.style).toContain("foo-bar") + [`MemberExpression[property.name=style][parent.parent.property.name=toContain][parent.callee.name=expect]`]( + node + ) { + const [styleName] = node.parent.parent.parent.arguments; + const matcher = node.parent.parent.property; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceText( + styleName, + `{${camelCase(styleName.value)}: expect.anything()}` + ), + ]; + }, + }); + }, + // expect(el.style).not.toContain("foo-bar") + [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toContain]`]( + node + ) { + const [styleName] = node.parent.parent.parent.parent.arguments; + const matcher = node.parent.parent.parent.property; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceText( + styleName, + `{${camelCase(styleName.value)}: expect.anything()}` + ), + ]; + }, + }); + }, + + //expect(el).toHaveAttribute("style", "foo: bar"); + [`CallExpression[callee.property.name][arguments.0.value=style][arguments.1]`]( + node + ) { + context.report({ + node: node.arguments[0], + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.replaceText(node.callee.property, "toHaveStyle"), + fixer.removeRange([ + node.arguments[0].range[0], + node.arguments[1].range[0], + ]), + ]; + }, + }); + }, + + //expect(el.style["foo-bar"]).toBe("baz") + [`MemberExpression[property.name=style][parent.computed=true][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.callee.name=expect]`]( + node + ) { + const styleName = node.parent.property; + const [styleValue] = node.parent.parent.parent.parent.arguments; + const matcher = node.parent.parent.parent.property; + const startOfStyleMemberExpression = node.object.range[1]; + const endOfStyleMemberExpression = node.parent.parent.arguments[0].range[1]; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([ + startOfStyleMemberExpression, + endOfStyleMemberExpression, + ]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceText( + styleValue, + `{${camelCase(styleName.value)}: ${styleValue.raw}}` + ), + ]; + }, + }); + }, + //expect(el.style["foo-bar"]).not.toBe("baz") + [`MemberExpression[property.name=style][parent.computed=true][parent.parent.parent.property.name=not][parent.parent.parent.parent.parent.callee.property.name=/toBe$|to(Strict)?Equal/][parent.parent.callee.name=expect]`]( + node + ) { + const styleName = node.parent.property; + const [styleValue] = node.parent.parent.parent.parent.parent.arguments; + const matcher = node.parent.parent.parent.parent.property; + const endOfStyleMemberExpression = node.parent.parent.arguments[0].range[1]; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], endOfStyleMemberExpression]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceText( + styleValue, + `{${camelCase(styleName.value)}: ${styleValue.raw}}` + ), + ]; + }, + }); + }, + //expect(foo.style).toHaveProperty("foo", "bar") + [`MemberExpression[property.name=style][parent.parent.property.name=toHaveProperty][parent.callee.name=expect]`]( + node + ) { + const [styleName, styleValue] = node.parent.parent.parent.arguments; + const matcher = node.parent.parent.property; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceTextRange( + [styleName.range[0], styleValue.range[1]], + `{${camelCase(styleName.value)}: ${styleValue.raw}}` + ), + ]; + }, + }); + }, + + //expect(foo.style).not.toHaveProperty("foo", "bar") + [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toHaveProperty][parent.callee.name=expect]`]( + node + ) { + const [styleName, styleValue] = node.parent.parent.parent.parent.arguments; + const matcher = node.parent.parent.parent.property; + + context.report({ + node: node.property, + message: "Use toHaveStyle instead of asserting on element style", + fix(fixer) { + return [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText(matcher, "toHaveStyle"), + fixer.replaceTextRange( + [styleName.range[0], styleValue.range[1]], + `{${camelCase(styleName.value)}: ${styleValue.raw}}` + ), + ]; + }, + }); + }, +}); diff --git a/src/rules/prefer-to-have-text-content.js b/src/rules/prefer-to-have-text-content.js index 44dd968..ad654b6 100644 --- a/src/rules/prefer-to-have-text-content.js +++ b/src/rules/prefer-to-have-text-content.js @@ -5,6 +5,8 @@ export const meta = { docs: { + category: "jest-dom", + url: "prefer-to-have-text-content", description: "Prefer toHaveTextContent over checking element.textContent", recommended: true, }, @@ -23,10 +25,7 @@ export const create = (context) => ({ message: `Use toHaveTextContent instead of asserting on DOM node attributes`, fix: (fixer) => { return [ - fixer.removeRange([ - node.property.range[0] - 1, - node.property.range[1], - ]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceTextRange( node.parent.parent.property.range, "toHaveTextContent" @@ -54,7 +53,7 @@ export const create = (context) => ({ node: node.parent, message: `Use toHaveTextContent instead of asserting on DOM node attributes`, fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceTextRange( node.parent.parent.property.range, "toHaveTextContent" @@ -69,7 +68,7 @@ export const create = (context) => ({ node: node.parent, message: `Use toHaveTextContent instead of asserting on DOM node attributes`, fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceTextRange( node.parent.parent.parent.property.range, "toHaveTextContent" @@ -86,7 +85,7 @@ export const create = (context) => ({ node: node.parent, message: `Use toHaveTextContent instead of asserting on DOM node attributes`, fix: (fixer) => [ - fixer.removeRange([node.property.range[0] - 1, node.property.range[1]]), + fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceTextRange( node.parent.parent.parent.property.range, "toHaveTextContent"