From 5c8c3430b048390297b6abb58cdf0ca4842f491e Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Tue, 1 Sep 2020 07:58:16 -0700 Subject: [PATCH] fix: better handling of template literals --- .../lib/rules/prefer-to-have-style.js | 21 +- src/rules/prefer-empty.js | 295 +++++++++--------- src/rules/prefer-to-have-style.js | 21 +- 3 files changed, 179 insertions(+), 158 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index e800a30..75e447c 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -5,7 +5,7 @@ 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, { +ruleTester.run("prefer-to-have-style", rule, { valid: [ `expect(el).toHaveStyle({foo:"bar"})`, `expect(el.style).toMatchSnapshot()`, @@ -23,6 +23,16 @@ ruleTester.run("prefer-to-have-attribute", rule, { errors, output: `expect(el).not.toHaveStyle({foo:"bar"})`, }, + { + code: "expect(el.style.backgroundImage).toBe(`url(${foo})`)", + errors, + output: "expect(el).toHaveStyle({backgroundImage:`url(${foo})`})", + }, + { + code: "expect(el.style.backgroundImage).not.toBe(`url(${foo})`)", + errors, + output: "expect(el).not.toHaveStyle({backgroundImage:`url(${foo})`})", + }, { code: `expect(el.style).toHaveProperty("background-color", "green")`, errors, @@ -39,9 +49,14 @@ ruleTester.run("prefer-to-have-attribute", rule, { output: `expect(screen.getByTestId("foo")).toHaveStyle({scrollSnapType: "x mandatory"})`, }, { - code: `expect(screen.getByTestId("foo").style["scroll-snap-type"]).not.toBe("x mandatory")`, + code: 'expect(el.style["scroll-snap-type"]).toBe(`${x} mandatory`)', + errors, + output: "expect(el).toHaveStyle({scrollSnapType: `${x} mandatory`})", + }, + { + code: `expect(el.style["scroll-snap-type"]).not.toBe("x mandatory")`, errors, - output: `expect(screen.getByTestId("foo")).not.toHaveStyle({scrollSnapType: "x mandatory"})`, + output: `expect(el).not.toHaveStyle({scrollSnapType: "x mandatory"})`, }, { code: `expect(el.style).toContain("background-color")`, diff --git a/src/rules/prefer-empty.js b/src/rules/prefer-empty.js index e9d2009..8a973e6 100644 --- a/src/rules/prefer-empty.js +++ b/src/rules/prefer-empty.js @@ -13,157 +13,156 @@ export const meta = { fixable: "code", // or "code" or "whitespace" }; -export const create = (context) => ({ - [`BinaryExpression[left.property.name='innerHTML'][right.value=''][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`]( - node - ) { - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - fixer.removeRange([node.left.object.range[1], node.range[1]]), - fixer.replaceText( - node.parent.parent.property, - Boolean(node.parent.parent.parent.arguments[0].value) === - node.operator.startsWith("=") // binary expression XNOR matcher boolean - ? "toBeEmptyDOMElement" - : "not.toBeEmptyDOMElement" - ), - fixer.remove(node.parent.parent.parent.arguments[0]), - ], - }); - }, - [`BinaryExpression[left.property.name='firstChild'][right.value=null][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`]( - node - ) { - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - fixer.removeRange([node.left.object.range[1], node.range[1]]), - fixer.replaceText( - node.parent.parent.property, - Boolean(node.parent.parent.parent.arguments[0].value) === - node.operator.startsWith("=") // binary expression XNOR matcher boolean - ? "toBeEmptyDOMElement" - : "not.toBeEmptyDOMElement" - ), - fixer.remove(node.parent.parent.parent.arguments[0]), - ], - }); - }, - [`MemberExpression[property.name = 'innerHTML'][parent.callee.name = 'expect'][parent.parent.property.name = /toBe$|to(Strict)?Equal/]`]( - node - ) { - const args = node.parent.parent.parent.arguments[0]; - if (isNonEmptyStringOrTemplateLiteral(args)) { - return; - } +export const create = (context) => { + function isNonEmptyStringOrTemplateLiteral(node) { + return !['""', "''", "``", "null"].includes( + context.getSourceCode().getText(node) + ); + } - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - 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]), - ], - }); - }, + return { + [`BinaryExpression[left.property.name='innerHTML'][right.value=''][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`]( + node + ) { + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + fixer.removeRange([node.left.object.range[1], node.range[1]]), + fixer.replaceText( + node.parent.parent.property, + Boolean(node.parent.parent.parent.arguments[0].value) === + node.operator.startsWith("=") // binary expression XNOR matcher boolean + ? "toBeEmptyDOMElement" + : "not.toBeEmptyDOMElement" + ), + fixer.remove(node.parent.parent.parent.arguments[0]), + ], + }); + }, + [`BinaryExpression[left.property.name='firstChild'][right.value=null][parent.callee.name='expect'][parent.parent.property.name=/toBe$|to(Strict)?Equal/]`]( + node + ) { + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + fixer.removeRange([node.left.object.range[1], node.range[1]]), + fixer.replaceText( + node.parent.parent.property, + Boolean(node.parent.parent.parent.arguments[0].value) === + node.operator.startsWith("=") // binary expression XNOR matcher boolean + ? "toBeEmptyDOMElement" + : "not.toBeEmptyDOMElement" + ), + fixer.remove(node.parent.parent.parent.arguments[0]), + ], + }); + }, + [`MemberExpression[property.name = 'innerHTML'][parent.callee.name = 'expect'][parent.parent.property.name = /toBe$|to(Strict)?Equal/]`]( + node + ) { + const args = node.parent.parent.parent.arguments[0]; - [`MemberExpression[property.name='innerHTML'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal$/][parent.parent.object.callee.name='expect']`]( - node - ) { - const args = node.parent.parent.parent.parent.arguments[0]; - if (isNonEmptyStringOrTemplateLiteral(args)) { - return; - } + if (isNonEmptyStringOrTemplateLiteral(args)) { + return; + } - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - fixer.removeRange([node.object.range[1], node.property.range[1]]), - fixer.replaceText( - node.parent.parent.parent.property, - "toBeEmptyDOMElement" - ), - fixer.remove(node.parent.parent.parent.parent.arguments[0]), - ], - }); - }, - [`MemberExpression[property.name = 'firstChild'][parent.callee.name = 'expect'][parent.parent.property.name = /toBeNull$/]`]( - node - ) { - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - fixer.removeRange([node.object.range[1], node.property.range[1]]), - fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"), - ], - }); - }, - [`MemberExpression[property.name='firstChild'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal$/][parent.parent.object.callee.name='expect']`]( - node - ) { - if (node.parent.parent.parent.parent.arguments[0].value !== null) { - return; - } + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + 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]), + ], + }); + }, - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - fixer.removeRange([node.object.range[1], node.property.range[1]]), - fixer.replaceText( - node.parent.parent.parent.property, - "toBeEmptyDOMElement" - ), - fixer.remove(node.parent.parent.parent.parent.arguments[0]), - ], - }); - }, - [`MemberExpression[property.name='firstChild'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBeNull$/][parent.parent.object.callee.name='expect']`]( - node - ) { - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - fixer.removeRange([node.object.range[1], node.property.range[1]]), - fixer.replaceText( - node.parent.parent.parent.property, - "toBeEmptyDOMElement" - ), - ], - }); - }, - [`MemberExpression[property.name = 'firstChild'][parent.callee.name = 'expect'][parent.parent.property.name = /toBe$|to(Strict)?Equal/]`]( - node - ) { - if (node.parent.parent.parent.arguments[0].value !== null) { - return; - } + [`MemberExpression[property.name='innerHTML'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal$/][parent.parent.object.callee.name='expect']`]( + node + ) { + const args = node.parent.parent.parent.parent.arguments[0]; + if (isNonEmptyStringOrTemplateLiteral(args)) { + return; + } - context.report({ - node, - message: "Use toBeEmptyDOMElement instead of checking inner html.", - fix: (fixer) => [ - 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]), - ], - }); - }, -}); + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText( + node.parent.parent.parent.property, + "toBeEmptyDOMElement" + ), + fixer.remove(node.parent.parent.parent.parent.arguments[0]), + ], + }); + }, + [`MemberExpression[property.name = 'firstChild'][parent.callee.name = 'expect'][parent.parent.property.name = /toBeNull$/]`]( + node + ) { + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText(node.parent.parent.property, "toBeEmptyDOMElement"), + ], + }); + }, + [`MemberExpression[property.name='firstChild'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal$/][parent.parent.object.callee.name='expect']`]( + node + ) { + if (node.parent.parent.parent.parent.arguments[0].value !== null) { + return; + } -function isNonEmptyStringOrTemplateLiteral(node) { - return ( - !(node.type === "Literal" || node.type === "TemplateLiteral") || - node.value || - node.name || - (node?.quasis?.length > 0 && - !(node?.quasis?.length === 1 && node?.quasis[0]?.value?.raw === "")) - ); -} + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText( + node.parent.parent.parent.property, + "toBeEmptyDOMElement" + ), + fixer.remove(node.parent.parent.parent.parent.arguments[0]), + ], + }); + }, + [`MemberExpression[property.name='firstChild'][parent.parent.property.name='not'][parent.parent.parent.property.name=/toBeNull$/][parent.parent.object.callee.name='expect']`]( + node + ) { + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + fixer.removeRange([node.object.range[1], node.property.range[1]]), + fixer.replaceText( + node.parent.parent.parent.property, + "toBeEmptyDOMElement" + ), + ], + }); + }, + [`MemberExpression[property.name = 'firstChild'][parent.callee.name = 'expect'][parent.parent.property.name = /toBe$|to(Strict)?Equal/]`]( + node + ) { + if (node.parent.parent.parent.arguments[0].value !== null) { + return; + } + + context.report({ + node, + message: "Use toBeEmptyDOMElement instead of checking inner html.", + fix: (fixer) => [ + 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-to-have-style.js b/src/rules/prefer-to-have-style.js index 4bc91db..ef20a7e 100644 --- a/src/rules/prefer-to-have-style.js +++ b/src/rules/prefer-to-have-style.js @@ -34,7 +34,7 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceText( styleValue, - `{${styleName.name}:${styleValue.raw}}` + `{${styleName.name}:${context.getSourceCode().getText(styleValue)}}` ), ]; }, @@ -57,7 +57,7 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceText( styleValue, - `{${styleName.name}:${styleValue.raw}}` + `{${styleName.name}:${context.getSourceCode().getText(styleValue)}}` ), ]; }, @@ -136,7 +136,6 @@ export const create = (context) => ({ 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", @@ -149,7 +148,9 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceText( styleValue, - `{${camelCase(styleName.value)}: ${styleValue.raw}}` + `{${camelCase(styleName.value)}: ${context + .getSourceCode() + .getText(styleValue)}}` ), ]; }, @@ -173,7 +174,9 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceText( styleValue, - `{${camelCase(styleName.value)}: ${styleValue.raw}}` + `{${camelCase(styleName.value)}: ${context + .getSourceCode() + .getText(styleValue)}}` ), ]; }, @@ -195,7 +198,9 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceTextRange( [styleName.range[0], styleValue.range[1]], - `{${camelCase(styleName.value)}: ${styleValue.raw}}` + `{${camelCase(styleName.value)}: ${context + .getSourceCode() + .getText(styleValue)}}` ), ]; }, @@ -218,7 +223,9 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceTextRange( [styleName.range[0], styleValue.range[1]], - `{${camelCase(styleName.value)}: ${styleValue.raw}}` + `{${camelCase(styleName.value)}: ${context + .getSourceCode() + .getText(styleValue)}}` ), ]; },