From 5667c093452db21e85832b7490e5333b620514c8 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 30 Nov 2020 16:25:44 -0800 Subject: [PATCH 01/10] fix(prefer-in-document): more fixes to prefer-in-document --- src/__tests__/lib/rules/prefer-in-document.js | 9 ++++++++- src/rules/prefer-in-document.js | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/__tests__/lib/rules/prefer-in-document.js b/src/__tests__/lib/rules/prefer-in-document.js index 6b659f6..809dfa7 100644 --- a/src/__tests__/lib/rules/prefer-in-document.js +++ b/src/__tests__/lib/rules/prefer-in-document.js @@ -46,6 +46,11 @@ const valid = [ expect(foo).toHaveLength(1);`, `expect(screen.notAQuery('foo-bar')).toHaveLength(1)`, `expect(screen.getAllByText('foo-bar')).toHaveLength(2)`, + `import foo from "./foo"; + it('should be defined', () => { + expect(useBoolean).toBeDefined(); + }); + `, ]; const invalid = [ // Invalid cases that applies to all variants @@ -218,7 +223,9 @@ const invalid = [ ), ]; -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2017 } }); +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2017, sourceType: "module" }, +}); ruleTester.run("prefer-in-document", rule, { valid: [].concat(...valid), invalid: [].concat(...invalid), diff --git a/src/rules/prefer-in-document.js b/src/rules/prefer-in-document.js index 3c62cbc..0a94bd9 100644 --- a/src/rules/prefer-in-document.js +++ b/src/rules/prefer-in-document.js @@ -83,6 +83,7 @@ export const create = (context) => { function getQueryNodeFromAssignment(identifierName) { const variable = context.getScope().set.get(identifierName); + if (!variable) return; const init = variable.defs[0].node.init; let queryNode; From 2ee17786668553f134c7435ee8e43b825cccb0d1 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 30 Nov 2020 17:43:33 -0800 Subject: [PATCH 02/10] handle typescript parsing --- src/__tests__/lib/rules/prefer-in-document.js | 28 +++++++++++++++++-- src/rules/prefer-in-document.js | 22 +++++++-------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-in-document.js b/src/__tests__/lib/rules/prefer-in-document.js index 809dfa7..f43b0ac 100644 --- a/src/__tests__/lib/rules/prefer-in-document.js +++ b/src/__tests__/lib/rules/prefer-in-document.js @@ -49,8 +49,8 @@ const valid = [ `import foo from "./foo"; it('should be defined', () => { expect(useBoolean).toBeDefined(); - }); - `, + })`, + `const span = foo('foo') as HTMLSpanElement`, ]; const invalid = [ // Invalid cases that applies to all variants @@ -177,6 +177,7 @@ const invalid = [ expect(await screen.findByText(/Compressing video/)).not.toBeInTheDocument(); })` ), + invalidCase( `it("foo", async () => { const compressingFeedback = await screen.findByText(/Compressing video/); @@ -221,10 +222,31 @@ const invalid = [ expect(compressingFeedback).not.toBeInTheDocument(); });` ), + invalidCase( + `const span = getByText('foo') as HTMLSpanElement + expect(span).not.toBeNull()`, + `const span = getByText('foo') as HTMLSpanElement + expect(span).toBeInTheDocument()` + ), + invalidCase( + `const span = await findByText('foo') as HTMLSpanElement + expect(span).not.toBeNull()`, + `const span = await findByText('foo') as HTMLSpanElement + expect(span).toBeInTheDocument()` + ), + invalidCase( + `let span; + span = getByText('foo') as HTMLSpanElement + expect(span).not.toBeNull()`, + `let span; + span = getByText('foo') as HTMLSpanElement + expect(span).toBeInTheDocument()` + ), ]; const ruleTester = new RuleTester({ - parserOptions: { ecmaVersion: 2017, sourceType: "module" }, + parser: require.resolve("@typescript-eslint/parser"), + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, }); ruleTester.run("prefer-in-document", rule, { valid: [].concat(...valid), diff --git a/src/rules/prefer-in-document.js b/src/rules/prefer-in-document.js index 0a94bd9..b1d2e88 100644 --- a/src/rules/prefer-in-document.js +++ b/src/rules/prefer-in-document.js @@ -81,6 +81,14 @@ export const create = (context) => { } } + function getQueryNodeFrom(expression) { + return expression.type === "TSAsExpression" + ? getQueryNodeFrom(expression.expression) + : expression.type === "AwaitExpression" + ? getQueryNodeFrom(expression.argument) + : expression.callee; + } + function getQueryNodeFromAssignment(identifierName) { const variable = context.getScope().set.get(identifierName); if (!variable) return; @@ -89,10 +97,7 @@ export const create = (context) => { let queryNode; if (init) { // let foo = screen.(); - queryNode = - init.type === "AwaitExpression" - ? init.argument.callee.property - : init.callee.property || init.callee; + queryNode = getQueryNodeFrom(init); } else { // let foo; // foo = screen.(); @@ -102,12 +107,8 @@ export const create = (context) => { if (!assignmentRef) { return; } - queryNode = - assignmentRef.writeExpr.type === "AwaitExpression" - ? assignmentRef.writeExpr.argument.callee - : assignmentRef.writeExpr.type === "CallExpression" - ? assignmentRef.writeExpr.callee - : assignmentRef.writeExpr; + const assignment = assignmentRef.writeExpr; + queryNode = getQueryNodeFrom(assignment); } return queryNode; } @@ -131,7 +132,6 @@ export const create = (context) => { expect, }); }, - // // const foo = expect(foo).not. [`MemberExpression[object.object.callee.name=expect][object.property.name=not][property.name=${alternativeMatchers}][object.object.arguments.0.type=Identifier]`]( node From 26b176962b997f1787cea13e79b1dfad1daef505 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 30 Nov 2020 18:50:08 -0800 Subject: [PATCH 03/10] fixes issues in to-have-style --- src/__tests__/lib/rules/prefer-in-document.js | 17 ++++++++++++ .../lib/rules/prefer-to-have-style.js | 10 +++++++ src/rules/prefer-to-have-style.js | 26 +++++++++++++------ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-in-document.js b/src/__tests__/lib/rules/prefer-in-document.js index f43b0ac..6852f35 100644 --- a/src/__tests__/lib/rules/prefer-in-document.js +++ b/src/__tests__/lib/rules/prefer-in-document.js @@ -51,6 +51,23 @@ const valid = [ expect(useBoolean).toBeDefined(); })`, `const span = foo('foo') as HTMLSpanElement`, + `const rtl = render() + const stars = rtl.container.querySelector('div').children + + expect(rtl.container.children).toHaveLength(1) + expect(stars).toHaveLength(5)`, + ` let content = container.querySelector('p') + + expect(content).not.toBeNull() + + fireEvent.click(closeButton) + + await waitExpect( + () => { + content = container.querySelector('p') + expect(content).toBeNull() + } + )`, ]; const invalid = [ // Invalid cases that applies to all variants diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index af5a90d..a37af61 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -16,12 +16,22 @@ ruleTester.run("prefer-to-have-style", rule, { document.body.setAttribute("style", "foo"); } }, [foo]);`, + `expect(collapse.style).not.toContain( + expect.objectContaining({ + display: 'none', + height: '0px', + }) + )`, ], invalid: [ { code: `expect(a.style).toHaveProperty('transform')`, errors, }, + { + code: `expect(a.style).not.toHaveProperty('transform')`, + errors, + }, { code: `expect(el.style.foo).toBe("bar")`, errors, diff --git a/src/rules/prefer-to-have-style.js b/src/rules/prefer-to-have-style.js index 98d338b..7ec8ba7 100644 --- a/src/rules/prefer-to-have-style.js +++ b/src/rules/prefer-to-have-style.js @@ -19,7 +19,7 @@ export const meta = { 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]`]( + [`MemberExpression[property.name=style][parent.computed=false][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal/][parent.parent.callee.name=expect]`]( node ) { const styleName = node.parent.property; @@ -41,13 +41,12 @@ export const create = (context) => ({ }); }, //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]`]( + [`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.parent.parent.parent.arguments.0.type=/(Template)?Literal$/][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", @@ -64,7 +63,7 @@ export const create = (context) => ({ }); }, // expect(el.style).toContain("foo-bar") - [`MemberExpression[property.name=style][parent.parent.property.name=toContain][parent.callee.name=expect]`]( + [`MemberExpression[property.name=style][parent.parent.property.name=toContain][parent.parent.parent.arguments.0.type=/(Template)?Literal$/][parent.callee.name=expect]`]( node ) { const [styleName] = node.parent.parent.parent.arguments; @@ -86,7 +85,7 @@ export const create = (context) => ({ }); }, // expect(el.style).not.toContain("foo-bar") - [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toContain]`]( + [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toContain][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal$/]`]( node ) { const [styleName] = node.parent.parent.parent.parent.arguments; @@ -128,7 +127,7 @@ export const create = (context) => ({ }, //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]`]( + [`MemberExpression[property.name=style][parent.computed=true][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal/][parent.parent.callee.name=expect]`]( node ) { const styleName = node.parent.property; @@ -157,7 +156,7 @@ export const create = (context) => ({ }); }, //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]`]( + [`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.parent.parent.parent.arguments.0.type=/(Template)?Literal/][parent.parent.callee.name=expect]`]( node ) { const styleName = node.parent.property; @@ -193,7 +192,11 @@ export const create = (context) => ({ node: node.property, message: "Use toHaveStyle instead of asserting on element style", fix(fixer) { - if (!styleValue) { + if ( + !styleValue || + (styleValue.type !== "Literal" && + styleValue.type !== "TemplateLiteral") + ) { return null; } return [ @@ -221,6 +224,13 @@ export const create = (context) => ({ node: node.property, message: "Use toHaveStyle instead of asserting on element style", fix(fixer) { + if ( + !styleValue || + (styleValue.type !== "Literal" && + styleValue.type !== "TemplateLiteral") + ) { + return null; + } return [ fixer.removeRange([node.object.range[1], node.property.range[1]]), fixer.replaceText(matcher, "toHaveStyle"), From 3aaea922aa14442a0757d938262112cff0a7de7a Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 30 Nov 2020 18:56:02 -0800 Subject: [PATCH 04/10] covg --- src/__tests__/lib/rules/prefer-to-have-style.js | 4 ++++ src/rules/prefer-to-have-style.js | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index a37af61..129b52f 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -32,6 +32,10 @@ ruleTester.run("prefer-to-have-style", rule, { code: `expect(a.style).not.toHaveProperty('transform')`, errors, }, + { + code: `expect(a.style).not.toHaveProperty(\`\${foo}\`)`, + errors, + }, { code: `expect(el.style.foo).toBe("bar")`, errors, diff --git a/src/rules/prefer-to-have-style.js b/src/rules/prefer-to-have-style.js index 7ec8ba7..4afc527 100644 --- a/src/rules/prefer-to-have-style.js +++ b/src/rules/prefer-to-have-style.js @@ -194,8 +194,7 @@ export const create = (context) => ({ fix(fixer) { if ( !styleValue || - (styleValue.type !== "Literal" && - styleValue.type !== "TemplateLiteral") + !["Literal", "TemplateLiteral"].includes(styleValue.type) ) { return null; } @@ -226,8 +225,7 @@ export const create = (context) => ({ fix(fixer) { if ( !styleValue || - (styleValue.type !== "Literal" && - styleValue.type !== "TemplateLiteral") + !["Literal", "TemplateLiteral"].includes(styleValue.type) ) { return null; } From e54bac31e53b64ed372f658550a1b6204ff56eb6 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 30 Nov 2020 19:21:52 -0800 Subject: [PATCH 05/10] handle allBy --- src/__tests__/lib/rules/prefer-in-document.js | 175 +++++++++++++----- src/rules/prefer-in-document.js | 12 ++ 2 files changed, 145 insertions(+), 42 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-in-document.js b/src/__tests__/lib/rules/prefer-in-document.js index 6852f35..9f053f7 100644 --- a/src/__tests__/lib/rules/prefer-in-document.js +++ b/src/__tests__/lib/rules/prefer-in-document.js @@ -71,48 +71,133 @@ const valid = [ ]; const invalid = [ // Invalid cases that applies to all variants - ...["getByText", "getAllByRole"].map((q) => [ - invalidCase( - `expect(screen.${q}('foo')).toHaveLength(1)`, - `expect(screen.${q}('foo')).toBeInTheDocument()` - ), - invalidCase( - `expect(${q}('foo')).toHaveLength(1)`, - `expect(${q}('foo')).toBeInTheDocument()` - ), - invalidCase( - `expect(wrapper.${q}('foo')).toHaveLength(1)`, - `expect(wrapper.${q}('foo')).toBeInTheDocument()` - ), - invalidCase( - `const foo = screen.${q}('foo'); - expect(foo).toHaveLength(1);`, - `const foo = screen.${q}('foo'); - expect(foo).toBeInTheDocument();` - ), - invalidCase( - `const foo = ${q}('foo'); - expect(foo).toHaveLength(1);`, - `const foo = ${q}('foo'); - expect(foo).toBeInTheDocument();` - ), - invalidCase( - `let foo; - foo = ${q}('foo'); - expect(foo).toHaveLength(1);`, - `let foo; - foo = ${q}('foo'); - expect(foo).toBeInTheDocument();` - ), - invalidCase( - `let foo; - foo = screen.${q}('foo'); - expect(foo).toHaveLength(1);`, - `let foo; - foo = screen.${q}('foo'); - expect(foo).toBeInTheDocument();` - ), - ]), + + invalidCase( + `expect(screen.getByText('foo')).toHaveLength(1)`, + `expect(screen.getByText('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(getByText('foo')).toHaveLength(1)`, + `expect(getByText('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(wrapper.getByText('foo')).toHaveLength(1)`, + `expect(wrapper.getByText('foo')).toBeInTheDocument()` + ), + invalidCase( + `const foo = screen.getByText('foo'); + expect(foo).toHaveLength(1);`, + `const foo = screen.getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `const foo = getByText('foo'); + expect(foo).toHaveLength(1);`, + `const foo = getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `let foo; + foo = getByText('foo'); + expect(foo).toHaveLength(1);`, + `let foo; + foo = getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `let foo; + foo = screen.getByText('foo'); + expect(foo).toHaveLength(1);`, + `let foo; + foo = screen.getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `expect(screen.getAllByRole('foo')).toHaveLength(1)`, + `expect(screen.getByRole('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(await screen.findAllByRole('foo')).toHaveLength(1)`, + `expect(await screen.findByRole('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(getAllByRole('foo')).toHaveLength(1)`, + `expect(getByRole('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(wrapper.getAllByRole('foo')).toHaveLength(1)`, + `expect(wrapper.getByRole('foo')).toBeInTheDocument()` + ), + invalidCase( + `const foo = screen.getAllByRole('foo'); + expect(foo).toHaveLength(1);`, + `const foo = screen.getByRole('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `const foo = getAllByRole('foo'); + expect(foo).toHaveLength(1);`, + `const foo = getByRole('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `let foo; + foo = getAllByRole('foo'); + expect(foo).toHaveLength(1);`, + `let foo; + foo = getByRole('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `let foo; + foo = screen.getAllByRole('foo'); + expect(foo).toHaveLength(1);`, + `let foo; + foo = screen.getByRole('foo'); + expect(foo).toBeInTheDocument();` + ), + + invalidCase( + `expect(screen.getByText('foo')).toHaveLength(1)`, + `expect(screen.getByText('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(getByText('foo')).toHaveLength(1)`, + `expect(getByText('foo')).toBeInTheDocument()` + ), + invalidCase( + `expect(wrapper.getByText('foo')).toHaveLength(1)`, + `expect(wrapper.getByText('foo')).toBeInTheDocument()` + ), + invalidCase( + `const foo = screen.getByText('foo'); + expect(foo).toHaveLength(1);`, + `const foo = screen.getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `const foo = getByText('foo'); + expect(foo).toHaveLength(1);`, + `const foo = getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `let foo; + foo = getByText('foo'); + expect(foo).toHaveLength(1);`, + `let foo; + foo = getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + invalidCase( + `let foo; + foo = screen.getByText('foo'); + expect(foo).toHaveLength(1);`, + `let foo; + foo = screen.getByText('foo'); + expect(foo).toBeInTheDocument();` + ), + // Invalid cases that applies to queryBy* and queryAllBy* ...["queryByText", "queryAllByText"].map((q) => [ invalidCase( @@ -259,6 +344,12 @@ const invalid = [ span = getByText('foo') as HTMLSpanElement expect(span).toBeInTheDocument()` ), + invalidCase( + `const things = screen.getAllByText("foo"); + expect(things).toHaveLength(1);`, + `const things = screen.getByText("foo"); + expect(things).toBeInTheDocument();` + ), ]; const ruleTester = new RuleTester({ diff --git a/src/rules/prefer-in-document.js b/src/rules/prefer-in-document.js index b1d2e88..dbc104c 100644 --- a/src/rules/prefer-in-document.js +++ b/src/rules/prefer-in-document.js @@ -56,6 +56,18 @@ export const create = (context) => { for (const argument of Array.from(matcherArguments)) { operations.push(fixer.remove(argument)); } + if ( + matcherNode.name === "toHaveLength" && + matcherArguments[0].value === 1 && + query.indexOf("All") > 0 + ) { + operations.push( + fixer.replaceText( + queryNode.property || queryNode, + query.replace("All", "") + ) + ); + } // Flip the .not if necessary if (isAntonymMatcher(matcherNode, matcherArguments)) { if (negatedMatcher) { From ab1e7c655ceb654a3ad5bb34963ae4b0c25a1fbf Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Mon, 30 Nov 2020 19:23:41 -0800 Subject: [PATCH 06/10] add DTL to repos --- smoke-test/repositories.json | 1 + 1 file changed, 1 insertion(+) diff --git a/smoke-test/repositories.json b/smoke-test/repositories.json index 3defa57..10c53ac 100644 --- a/smoke-test/repositories.json +++ b/smoke-test/repositories.json @@ -248,6 +248,7 @@ "Twistbioscience/DesignerComponents", "bopen/react-jsonschema-form-field-geolocation", "chuntley/dom-testing-extended", + "testing-library/dom-testing-library", "frankieyan/custom-meta-input", "villeheikkila/fullstackopen", "kentcdodds/learn-react", From 8dedd187cad5a065fa108fe6dfc210163c95be6c Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Tue, 1 Dec 2020 10:08:33 -0800 Subject: [PATCH 07/10] fix for template literals in style checks --- .../lib/rules/prefer-to-have-style.js | 15 ++++++++++++ src/rules/prefer-to-have-style.js | 24 ++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index 129b52f..acdac6d 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -96,5 +96,20 @@ ruleTester.run("prefer-to-have-style", rule, { errors, output: `expect(el).toHaveStyle("background-color: green; border-width: 10px; color: blue;")`, }, + { + code: `expect(imageElement.style[\`box-shadow\`]).toBe(\`inset 0px 0px 0px 400px \${c}\`)`, + errors, + output: `expect(imageElement).toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px \${c}\`)`, + }, + { + code: `expect(imageElement.style[\`box-shadow\` ]).toBe( \`inset 0px 0px 0px 400px \${c}\`)`, + errors, + output: `expect(imageElement).toHaveStyle( \`box-shadow: inset 0px 0px 0px 400px \${c}\`)`, + }, + { + code: `expect(imageElement.style[\`box-shadow\`]).not.toBe(\`inset 0px 0px 0px 400px \${c}\`)`, + errors, + output: `expect(imageElement).not.toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px \${c}\`)`, + }, ], }); diff --git a/src/rules/prefer-to-have-style.js b/src/rules/prefer-to-have-style.js index 4afc527..c221d49 100644 --- a/src/rules/prefer-to-have-style.js +++ b/src/rules/prefer-to-have-style.js @@ -147,9 +147,15 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceText( styleValue, - `{${camelCase(styleName.value)}: ${context - .getSourceCode() - .getText(styleValue)}}` + styleName.type === "Literal" + ? `{${camelCase( + styleName.value + )}: ${context.getSourceCode().getText(styleValue)}}` + : `${context.getSourceCode().getText(styleName).slice(0, -1)}: ${ + styleValue.type === "TemplateLiteral" + ? context.getSourceCode().getText(styleValue).substring(1) + : `${styleValue.value}\`` + }` ), ]; }, @@ -173,9 +179,15 @@ export const create = (context) => ({ fixer.replaceText(matcher, "toHaveStyle"), fixer.replaceText( styleValue, - `{${camelCase(styleName.value)}: ${context - .getSourceCode() - .getText(styleValue)}}` + styleName.type === "Literal" + ? `{${camelCase( + styleName.value + )}: ${context.getSourceCode().getText(styleValue)}}` + : `${context.getSourceCode().getText(styleName).slice(0, -1)}: ${ + styleValue.type === "TemplateLiteral" + ? context.getSourceCode().getText(styleValue).substring(1) + : `${styleValue.value}\`` + }` ), ]; }, From a8b5369db9cf550f2e40774c993d9b24fd9d03e6 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Tue, 1 Dec 2020 13:24:19 -0800 Subject: [PATCH 08/10] covg --- src/__tests__/lib/rules/prefer-to-have-style.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index acdac6d..819cf46 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -106,10 +106,20 @@ ruleTester.run("prefer-to-have-style", rule, { errors, output: `expect(imageElement).toHaveStyle( \`box-shadow: inset 0px 0px 0px 400px \${c}\`)`, }, + { + code: `expect(imageElement.style[\`box-\${shadow}\`]).toBe("inset 0px 0px 0px 400px 40px")`, + errors, + output: `expect(imageElement).toHaveStyle(\`box-\${shadow}: inset 0px 0px 0px 400px 40px\`)`, + }, { code: `expect(imageElement.style[\`box-shadow\`]).not.toBe(\`inset 0px 0px 0px 400px \${c}\`)`, errors, output: `expect(imageElement).not.toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px \${c}\`)`, }, + { + code: `expect(imageElement.style[\`box-shadow\`]).not.toBe("inset 0px 0px 0px 400px 40px")`, + errors, + output: `expect(imageElement).not.toHaveStyle(\`box-shadow: inset 0px 0px 0px 400px 40px\`)`, + }, ], }); From 19623da038ce9e6f5c952c152599845722c403e9 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Tue, 1 Dec 2020 16:54:25 -0800 Subject: [PATCH 09/10] another case --- .../lib/rules/prefer-to-have-style.js | 5 + src/rules/prefer-to-have-style.js | 474 +++++++++--------- 2 files changed, 249 insertions(+), 230 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index 819cf46..97c585a 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -86,6 +86,11 @@ ruleTester.run("prefer-to-have-style", rule, { errors, output: `expect(el).toHaveStyle({backgroundColor: expect.anything()})`, }, + { + code: `expect(el.style).toContain(\`background-color\`)`, + errors, + output: `expect(el).toHaveStyle(\`background-color\`)`, + }, { code: `expect(el.style).not.toContain("background-color")`, errors, diff --git a/src/rules/prefer-to-have-style.js b/src/rules/prefer-to-have-style.js index c221d49..fa7d0ae 100644 --- a/src/rules/prefer-to-have-style.js +++ b/src/rules/prefer-to-have-style.js @@ -17,241 +17,255 @@ export const meta = { 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.parent.parent.arguments.0.type=/(Template)?Literal/][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}:${context.getSourceCode().getText(styleValue)}}` - ), - ]; - }, - }); - }, - //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.parent.parent.parent.arguments.0.type=/(Template)?Literal$/][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}:${context.getSourceCode().getText(styleValue)}}` - ), - ]; - }, - }); - }, - // expect(el.style).toContain("foo-bar") - [`MemberExpression[property.name=style][parent.parent.property.name=toContain][parent.parent.parent.arguments.0.type=/(Template)?Literal$/][parent.callee.name=expect]`]( - node - ) { - const [styleName] = node.parent.parent.parent.arguments; - const matcher = node.parent.parent.property; +export const create = (context) => { + function getReplacementStyleParam(styleName, styleValue) { + return styleName.type === "Literal" + ? `{${camelCase(styleName.value)}: ${context + .getSourceCode() + .getText(styleValue)}}` + : `${context.getSourceCode().getText(styleName).slice(0, -1)}: ${ + styleValue.type === "TemplateLiteral" + ? context.getSourceCode().getText(styleValue).substring(1) + : `${styleValue.value}\`` + }`; + } - 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][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal$/]`]( - node - ) { - const [styleName] = node.parent.parent.parent.parent.arguments; - const matcher = node.parent.parent.parent.property; + return { + //expect(el.style.foo).toBe("bar"); + [`MemberExpression[property.name=style][parent.computed=false][parent.parent.parent.property.name=/toBe$|to(Strict)?Equal/][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal/][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}:${context + .getSourceCode() + .getText(styleValue)}}` + ), + ]; + }, + }); + }, + //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.parent.parent.parent.arguments.0.type=/(Template)?Literal$/][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}:${context + .getSourceCode() + .getText(styleValue)}}` + ), + ]; + }, + }); + }, + // expect(el.style).toContain("foo-bar") + [`MemberExpression[property.name=style][parent.parent.property.name=toContain][parent.parent.parent.arguments.0.type=/(Template)?Literal$/][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()}` - ), - ]; - }, - }); - }, + 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, + styleName.type === "Literal" + ? `{${camelCase(styleName.value)}: expect.anything()}` + : context.getSourceCode().getText(styleName) + ), + ]; + }, + }); + }, + // expect(el.style).not.toContain("foo-bar") + [`MemberExpression[property.name=style][parent.parent.property.name=not][parent.parent.parent.property.name=toContain][parent.parent.parent.parent.arguments.0.type=/(Template)?Literal$/]`]( + node + ) { + const [styleName] = node.parent.parent.parent.parent.arguments; + const matcher = node.parent.parent.parent.property; - //expect(el).toHaveAttribute("style", "foo: bar"); - [`CallExpression[callee.property.name=toHaveAttribute][arguments.0.value=style][arguments.1][callee.object.callee.name=expect]`]( - 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], - ]), - ]; - }, - }); - }, + 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, + styleName.type === "Literal" + ? `{${camelCase(styleName.value)}: expect.anything()}` + : context.getSourceCode().getText(styleName) + ), + ]; + }, + }); + }, - //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.parent.parent.arguments.0.type=/(Template)?Literal/][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, - styleName.type === "Literal" - ? `{${camelCase( - styleName.value - )}: ${context.getSourceCode().getText(styleValue)}}` - : `${context.getSourceCode().getText(styleName).slice(0, -1)}: ${ - styleValue.type === "TemplateLiteral" - ? context.getSourceCode().getText(styleValue).substring(1) - : `${styleValue.value}\`` - }` - ), - ]; - }, - }); - }, - //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.parent.parent.parent.arguments.0.type=/(Template)?Literal/][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]; + //expect(el).toHaveAttribute("style", "foo: bar"); + [`CallExpression[callee.property.name=toHaveAttribute][arguments.0.value=style][arguments.1][callee.object.callee.name=expect]`]( + 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], + ]), + ]; + }, + }); + }, - 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, - styleName.type === "Literal" - ? `{${camelCase( - styleName.value - )}: ${context.getSourceCode().getText(styleValue)}}` - : `${context.getSourceCode().getText(styleName).slice(0, -1)}: ${ - styleValue.type === "TemplateLiteral" - ? context.getSourceCode().getText(styleValue).substring(1) - : `${styleValue.value}\`` - }` - ), - ]; - }, - }); - }, - //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; + //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.parent.parent.arguments.0.type=/(Template)?Literal/][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, + getReplacementStyleParam(styleName, styleValue) + ), + ]; + }, + }); + }, + //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.parent.parent.parent.arguments.0.type=/(Template)?Literal/][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) { - if ( - !styleValue || - !["Literal", "TemplateLiteral"].includes(styleValue.type) - ) { - return null; - } - 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)}: ${context - .getSourceCode() - .getText(styleValue)}}` - ), - ]; - }, - }); - }, + 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, + getReplacementStyleParam(styleName, styleValue) + ), + ]; + }, + }); + }, + //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; - //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) { + if ( + !styleValue || + !["Literal", "TemplateLiteral"].includes(styleValue.type) + ) { + return null; + } + 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 + )}: ${context.getSourceCode().getText(styleValue)}}` + ), + ]; + }, + }); + }, - context.report({ - node: node.property, - message: "Use toHaveStyle instead of asserting on element style", - fix(fixer) { - if ( - !styleValue || - !["Literal", "TemplateLiteral"].includes(styleValue.type) - ) { - return null; - } - 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)}: ${context - .getSourceCode() - .getText(styleValue)}}` - ), - ]; - }, - }); - }, -}); + //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) { + if ( + !styleValue || + !["Literal", "TemplateLiteral"].includes(styleValue.type) + ) { + return null; + } + 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 + )}: ${context.getSourceCode().getText(styleValue)}}` + ), + ]; + }, + }); + }, + }; +}; From fd86d477ac7aaedd12ccbf5d661cf11abefe099b Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Tue, 1 Dec 2020 17:01:17 -0800 Subject: [PATCH 10/10] covg --- src/__tests__/lib/rules/prefer-to-have-style.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/__tests__/lib/rules/prefer-to-have-style.js b/src/__tests__/lib/rules/prefer-to-have-style.js index 97c585a..9c8c21a 100644 --- a/src/__tests__/lib/rules/prefer-to-have-style.js +++ b/src/__tests__/lib/rules/prefer-to-have-style.js @@ -91,6 +91,11 @@ ruleTester.run("prefer-to-have-style", rule, { errors, output: `expect(el).toHaveStyle(\`background-color\`)`, }, + { + code: `expect(el.style).not.toContain(\`background-color\`)`, + errors, + output: `expect(el).not.toHaveStyle(\`background-color\`)`, + }, { code: `expect(el.style).not.toContain("background-color")`, errors,