From 7506f9441e37c1f9909d21ca23f135c0d16d0238 Mon Sep 17 00:00:00 2001 From: Ben Monro Date: Thu, 3 Dec 2020 22:43:58 -0800 Subject: [PATCH] fix(rules): added more use cases for to have class and in docucment (#120) * fixes to have class and in docucment * removed unused code * handle the nots * Update src/rules/prefer-to-have-class.js --- src/__tests__/lib/rules/prefer-in-document.js | 6 ++ .../lib/rules/prefer-prefer-to-have-class.js | 74 +++++++++++++++++++ src/rules/prefer-in-document.js | 5 +- src/rules/prefer-to-have-class.js | 61 +++++++++++++-- 4 files changed, 138 insertions(+), 8 deletions(-) diff --git a/src/__tests__/lib/rules/prefer-in-document.js b/src/__tests__/lib/rules/prefer-in-document.js index 9f053f7..627c22e 100644 --- a/src/__tests__/lib/rules/prefer-in-document.js +++ b/src/__tests__/lib/rules/prefer-in-document.js @@ -68,6 +68,12 @@ const valid = [ expect(content).toBeNull() } )`, + `expect(await screen.findAllByRole("button")).toHaveLength( + NUM_BUTTONS + )`, + `expect(await screen.findAllByRole("button")).not.toHaveLength( + NUM_BUTTONS + )`, ]; const invalid = [ // Invalid cases that applies to all variants diff --git a/src/__tests__/lib/rules/prefer-prefer-to-have-class.js b/src/__tests__/lib/rules/prefer-prefer-to-have-class.js index cd0aee0..334e236 100644 --- a/src/__tests__/lib/rules/prefer-prefer-to-have-class.js +++ b/src/__tests__/lib/rules/prefer-prefer-to-have-class.js @@ -133,5 +133,79 @@ ruleTester.run("prefer-to-have-class", rule, { code: `expect(el.classList[0]).toContain(("fo"))`, errors, }, + + { + code: `expect(el.classList).toEqual(expect.objectContaining({0:"foo"}))`, + errors, + }, + + { + code: `expect(el.classList).toContain(className)`, + errors, + output: `expect(el).toHaveClass(className)`, + }, + { + code: `expect(el.classList).toContain("className")`, + errors, + output: `expect(el).toHaveClass("className")`, + }, + + { + code: `expect(el.classList).toContain(foo("bar"))`, + errors, + output: `expect(el).toHaveClass(foo("bar"))`, + }, + + { + code: `expect(el.classList.contains("foo")).toBe(false)`, + errors, + output: `expect(el).not.toHaveClass("foo")`, + }, + + { + code: `expect(el.classList.contains("foo")).toBe(true)`, + errors, + output: `expect(el).toHaveClass("foo")`, + }, + { + code: `expect(el.classList.contains("foo")).toBeTruthy()`, + errors, + output: `expect(el).toHaveClass("foo")`, + }, + { + code: `expect(el.classList).not.toContain("bar")`, + errors, + output: `expect(el).not.toHaveClass("bar")`, + }, + { + code: `expect(el.classList).not.toBe("bar")`, + errors, + }, + { + code: `expect(el.classList[0]).not.toContain(("fo"))`, + errors, + }, + + { + code: `expect(el.classList).not.toEqual(expect.objectContaining({0:"foo"}))`, + errors, + }, + + { + code: `expect(el.classList).not.toContain(className)`, + errors, + output: `expect(el).not.toHaveClass(className)`, + }, + { + code: `expect(el.classList).not.toContain("className")`, + errors, + output: `expect(el).not.toHaveClass("className")`, + }, + + { + code: `expect(el.classList).not.toContain(foo("bar"))`, + errors, + output: `expect(el).not.toHaveClass(foo("bar"))`, + }, ], }); diff --git a/src/rules/prefer-in-document.js b/src/rules/prefer-in-document.js index 82339a4..f219d30 100644 --- a/src/rules/prefer-in-document.js +++ b/src/rules/prefer-in-document.js @@ -38,7 +38,10 @@ export const create = (context) => { }) { if (!queryNode || (!queryNode.name && !queryNode.property)) return; // toHaveLength() is only invalid with 0 or 1 - if (matcherNode.name === "toHaveLength" && matcherArguments[0].value > 1) { + if ( + matcherNode.name === "toHaveLength" && + (matcherArguments[0].type !== "Literal" || matcherArguments[0].value > 1) + ) { return; } diff --git a/src/rules/prefer-to-have-class.js b/src/rules/prefer-to-have-class.js index 9637819..493348c 100644 --- a/src/rules/prefer-to-have-class.js +++ b/src/rules/prefer-to-have-class.js @@ -22,6 +22,41 @@ export const meta = { }; export const create = (context) => ({ + //expect(el.classList.contains("foo")).toBe(true) + [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.callee.object.property.name=classList][callee.object.arguments.0.callee.property.name=contains][callee.property.name=/toBe(Truthy|Falsy)?|to(Strict)?Equal/]`]( + node + ) { + const classValue = node.callee.object.arguments[0].arguments[0]; + const checkedProp = node.callee.object.arguments[0].callee.object.object; + const matcher = node.callee.property; + const [matcherArg] = node.arguments; + const [expectArg] = node.callee.object.arguments; + const isTruthy = + (matcher.name === "toBe" && matcherArg.value === true) || + matcher.name === "toBeTruthy"; + + context.report({ + node: matcher, + messageId, + fix(fixer) { + return [ + fixer.removeRange([checkedProp.range[1], expectArg.range[1]]), + + fixer.replaceText(matcher, `${isTruthy ? "" : "not."}toHaveClass`), + matcherArg + ? fixer.replaceText( + matcherArg, + context.getSourceCode().getText(classValue) + ) + : fixer.insertTextBeforeRange( + [node.range[1] - 1, node.range[1] - 1], + context.getSourceCode().getText(classValue) + ), + ]; + }, + }); + }, + //expect(el.classList[0]).toBe("bar") [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.object.property.name=classList][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/]`]( node @@ -62,8 +97,8 @@ export const create = (context) => ({ messageId, }); }, - //expect(el.className).toBe("bar") / toStrict?Equal / toContain - [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.property.name=/class(Name|List)/][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/]`]( + //expect(el.className | el.classList).toBe("bar") / toStrict?Equal / toContain + [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.property.name=/class(Name|List)/][callee.property.name=/toBe$|to(Strict)?Equal|toContain/]`]( node ) { const checkedProp = node.callee.object.arguments[0].property; @@ -71,6 +106,14 @@ export const create = (context) => ({ const matcher = node.callee.property; const classNameProp = node.callee.object.arguments[0].object; + // don't report here if using `expect.foo()` + + if ( + classValue.type === "CallExpression" && + classValue.callee.type === "MemberExpression" && + classValue.callee.object.name === "expect" + ) + return; context.report({ node: matcher, messageId, @@ -91,19 +134,21 @@ export const create = (context) => ({ }); }, - //expect(el.className).toEqual(expect.stringContaining("foo")) / toStrictEqual - [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.property.name=className][callee.property.name=/to(Strict)?Equal/][arguments.0.callee.object.name=expect][arguments.0.callee.property.name=stringContaining]`]( + //expect(el.className | el.classList).toEqual(expect.stringContaining("foo") | objectContaining) / toStrictEqual + [`CallExpression[callee.object.callee.name=expect][callee.object.arguments.0.property.name=/class(Name|List)/][callee.property.name=/to(Strict)?Equal/][arguments.0.callee.object.name=expect]`]( node ) { const className = node.callee.object.arguments[0].property; const [classValue] = node.arguments[0].arguments; const matcher = node.callee.property; const classNameProp = node.callee.object.arguments[0].object; + const matcherArg = node.arguments[0].callee.property; context.report({ node: matcher, messageId, fix(fixer) { + if (matcherArg.name !== "stringContaining") return; return [ fixer.removeRange([classNameProp.range[1], className.range[1]]), fixer.replaceText(matcher, "toHaveClass"), @@ -116,11 +161,10 @@ export const create = (context) => ({ }); }, - //expect(screen.getByRole("button").className).not.toBe("foo"); / toStrict?Equal / toContain - [`CallExpression[callee.object.object.callee.name=expect][callee.object.object.arguments.0.property.name=className][callee.object.property.name=not][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/]`]( + //expect(screen.getByRole("button").className | classList).not.toBe("foo"); / toStrict?Equal / toContain + [`CallExpression[callee.object.object.callee.name=expect][callee.object.object.arguments.0.property.name=/class(Name|List)/][callee.object.property.name=not][callee.property.name=/toBe$|to(Strict)?Equal|toContain/]`]( node ) { - //[callee.object.arguments.0.property.name=className][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/] const className = node.callee.object.object.arguments[0].property; const [classValue] = node.arguments; const matcher = node.callee.property; @@ -130,6 +174,9 @@ export const create = (context) => ({ node: matcher, messageId, fix(fixer) { + if (className.name === "classList" && matcher.name !== "toContain") + return; + return [ fixer.removeRange([classNameProp.range[1], className.range[1]]), fixer.replaceText(matcher, "toHaveClass"),