"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.meta = exports.create = void 0; var _assignmentAst = require("../assignment-ast"); /** * @fileoverview prefer toHaveClass over checking element className * @author Ben Monro */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ const messageId = "use-to-have-class"; const meta = { docs: { category: "Best Practices", url: "prefer-to-have-class", description: "prefer toHaveClass over checking element className", recommended: true }, messages: { [messageId]: `Prefer .toHaveClass() over checking element className` }, fixable: "code" }; exports.meta = meta; 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.insertTextBefore(context.getSourceCode().getTokenAfter(matcher, { skip: 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) { const [classValue] = node.arguments; const matcher = node.callee.property; const classNameProp = node.callee.object.arguments[0].object; const expectArg = node.callee.object.arguments[0]; context.report({ node: matcher, messageId, fix(fixer) { //can't autofix here as it toHaveClass doesn't have a partial matcher / regex for class names. if (matcher.name === "toContain") return; return [fixer.removeRange([classNameProp.object.range[1], expectArg.range[1]]), fixer.replaceText(matcher, "toHaveClass"), fixer.replaceText(classValue, context.getSourceCode().getText(classValue))]; } }); }, //expect(el.classList[0]).not.toBe("bar") [`CallExpression[callee.object.object.callee.name=expect][callee.object.object.arguments.0.object.property.name=classList][callee.object.property.name=not][callee.property.name=/toBe$|to(Strict)?Equal|toContain/][arguments.0.type=/Literal$/]`](node) { //can't autofix this case because the class could be in another element of the classList array. context.report({ node, messageId }); }, //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; const [classValue] = node.arguments; const matcher = node.callee.property; const classNameProp = node.callee.object.arguments[0].object; const { isDTLQuery } = (0, _assignmentAst.getQueryNodeFrom)(context, classNameProp); if (!isDTLQuery) return; // 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, fix(fixer) { if (checkedProp.name === "classList" && matcher.name !== "toContain") { return; } return [fixer.removeRange([classNameProp.range[1], checkedProp.range[1]]), fixer.replaceText(matcher, "toHaveClass"), fixer.replaceText(classValue, `${context.getSourceCode().getText(classValue)}${matcher.name === "toContain" ? "" : ", { exact: true }"}`)]; } }); }, //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; const { isDTLQuery } = (0, _assignmentAst.getQueryNodeFrom)(context, classNameProp); if (!isDTLQuery) return; 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"), fixer.replaceText(node.arguments[0], `${context.getSourceCode().getText(classValue)}`)]; } }); }, //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) { const className = node.callee.object.object.arguments[0].property; const [classValue] = node.arguments; const matcher = node.callee.property; const classNameProp = node.callee.object.object.arguments[0].object; const { isDTLQuery } = (0, _assignmentAst.getQueryNodeFrom)(context, classNameProp); if (!isDTLQuery) return; context.report({ 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"), fixer.replaceText(classValue, `${context.getSourceCode().getText(classValue)}${matcher.name === "toContain" ? "" : ", { exact: true }"}`)]; } }); }, //expect(el).toHaveProperty("className", "foo: bar"); //expect(el).toHaveAttribute("class", "foo: bar"); [[`CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveAttribute][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`, `CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveProperty][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`].join(",")](node) { const matcher = node.callee.property; const [classArg, classValueArg] = node.arguments; const classNameValue = context.getSourceCode().getText(classArg).slice(1, -1); if (matcher.name === "toHaveAttribute" && classNameValue !== "class" || matcher.name === "toHaveProperty" && classNameValue !== "className") { return; } const { isDTLQuery } = (0, _assignmentAst.getQueryNodeFrom)(context, node.callee.object.arguments[0]); if (!isDTLQuery) return; context.report({ node: matcher, messageId, fix(fixer) { return [fixer.replaceText(matcher, "toHaveClass"), fixer.replaceText(classArg, context.getSourceCode().getText(classValueArg)), fixer.replaceText(classValueArg, `{ exact: true }`)]; } }); }, //expect(el).not.toHaveAttribute("class", "foo: bar"); //expect(el).not.toHaveProperty("className", "foo: bar"); [[`CallExpression[callee.object.object.callee.name=expect][callee.object.property.name=not][callee.property.name=toHaveAttribute][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`, `CallExpression[callee.object.object.callee.name=expect][callee.object.property.name=not][callee.property.name=toHaveProperty][arguments.0.type=/Literal/][arguments.1.type=/Literal$/]`].join(",")](node) { //[callee.object.property.name=/toHaveAttribute|toHaveProperty/][arguments.0.value=class][arguments.1.type=/Literal$/] const matcher = node.callee.property; const [classArg, classValueArg] = node.arguments; const classNameValue = context.getSourceCode().getText(classArg).slice(1, -1); if (matcher.name === "toHaveAttribute" && classNameValue !== "class" || matcher.name === "toHaveProperty" && classNameValue !== "className") { return; } const { isDTLQuery } = (0, _assignmentAst.getQueryNodeFrom)(context, node.callee.object.object.arguments[0]); if (!isDTLQuery) return; context.report({ node: matcher, messageId, fix(fixer) { return [fixer.replaceText(matcher, "toHaveClass"), fixer.replaceText(classArg, context.getSourceCode().getText(classValueArg)), fixer.replaceText(classValueArg, `{ exact: true }`)]; } }); }, //expect(el).toHaveProperty(`className`, expect.stringContaining("foo")); //expect(el).toHaveAttribute(`class`, expect.stringContaining("foo")); [[`CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveAttribute][arguments.0.type=/Literal/][arguments.1.callee.object.name=expect][arguments.1.callee.property.name=stringContaining]`, `CallExpression[callee.object.callee.name=expect][callee.property.name=toHaveProperty][arguments.0.type=/Literal/][arguments.1.callee.object.name=expect][arguments.1.callee.property.name=stringContaining]`].join(",")](node) { const matcher = node.callee.property; const [classArg, classValue] = node.arguments; const classValueArg = classValue.arguments[0]; const classNameValue = context.getSourceCode().getText(classArg).slice(1, -1); if (matcher.name === "toHaveAttribute" && classNameValue !== "class" || matcher.name === "toHaveProperty" && classNameValue !== "className") { return; } const { isDTLQuery } = (0, _assignmentAst.getQueryNodeFrom)(context, node.callee.object.arguments[0]); if (!isDTLQuery) return; context.report({ node: matcher, messageId, fix(fixer) { return [fixer.replaceText(matcher, "toHaveClass"), fixer.replaceText(classArg, context.getSourceCode().getText(classValueArg)), fixer.removeRange([classArg.range[1], classValue.range[1]])]; } }); } }); exports.create = create;