207 lines
11 KiB
JavaScript
207 lines
11 KiB
JavaScript
"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; |