eslint-plugin-jest-dom
Advanced tools
Comparing version 5.0.1 to 5.0.2
@@ -26,4 +26,7 @@ "use strict"; | ||
messages: { | ||
"use-document": `Prefer .toBeInTheDocument() for asserting DOM node existence` | ||
} | ||
"use-document": `Prefer .toBeInTheDocument() for asserting DOM node existence`, | ||
"invalid-combination-length-1": `Invalid combination of {{ query }} and .toHaveLength(1). Did you mean to use {{ allQuery }}?`, | ||
"replace-query-with-all": `Replace {{ query }} with {{ allQuery }}` | ||
}, | ||
hasSuggestions: true | ||
}; | ||
@@ -42,2 +45,18 @@ exports.meta = meta; | ||
} | ||
/** | ||
* Extract the DTL query identifier from a call expression | ||
* | ||
* <query>() -> <query> | ||
* screen.<query>() -> <query> | ||
*/ | ||
function getDTLQueryIdentifierNode(callExpressionNode) { | ||
if (!callExpressionNode || callExpressionNode.type !== "CallExpression") { | ||
return null; | ||
} | ||
if (callExpressionNode.callee.type === "Identifier") { | ||
return callExpressionNode.callee; | ||
} | ||
return callExpressionNode.callee.property; | ||
} | ||
const create = context => { | ||
@@ -72,11 +91,49 @@ const alternativeMatchers = /^(toHaveLength|toBeDefined|toBeNull|toBe|toEqual|toBeTruthy|toBeFalsy)$/; | ||
// toHaveLength() is only invalid with 0 or 1 | ||
if (matcherNode.name === "toHaveLength" && matcherArguments.length) { | ||
// *By* query with .toHaveLength(0/1) matcher are considered violations | ||
// | ||
// | Selector type | .toHaveLength(1) | .toHaveLength(0) | | ||
// | ============= | =========================== | ===================================== | | ||
// | *By* query | Did you mean to use *AllBy* | Replace with .not.toBeInTheDocument() | | ||
// | *AllBy* query | Correct | Correct | ||
// | ||
// @see https://github.com/testing-library/eslint-plugin-jest-dom/issues/171 | ||
// | ||
if (matcherNode.name === "toHaveLength" && matcherArguments.length === 1) { | ||
const lengthValue = getLengthValue(matcherArguments); | ||
// isNotToHaveLengthZero represents .not.toHaveLength(0) which is a valid use of toHaveLength | ||
const isNotToHaveLengthZero = usesToHaveLengthZero(matcherNode, matcherArguments) && negatedMatcher; | ||
const isValidUseOfToHaveLength = isNotToHaveLengthZero || !["Literal", "Identifier"].includes(matcherArguments[0].type) || lengthValue === undefined || lengthValue > 1; | ||
if (isValidUseOfToHaveLength) { | ||
const queryName = queryNode.name || queryNode.property.name; | ||
const isSingleQuery = _queries.queries.includes(queryName) && !/AllBy/.test(queryName); | ||
const hasViolation = isSingleQuery && [1, 0].includes(lengthValue); | ||
if (!hasViolation) { | ||
return; | ||
} | ||
// If length === 1, report violation with suggestions | ||
// Otherwise fallback to default report | ||
if (lengthValue === 1) { | ||
const allQuery = queryName.replace("By", "AllBy"); | ||
return context.report({ | ||
node: matcherNode, | ||
messageId: "invalid-combination-length-1", | ||
data: { | ||
query: queryName, | ||
allQuery | ||
}, | ||
loc: matcherNode.loc, | ||
suggest: [{ | ||
messageId: "replace-query-with-all", | ||
data: { | ||
query: queryName, | ||
allQuery | ||
}, | ||
fix(fixer) { | ||
return fixer.replaceText(queryNode.property || queryNode, allQuery); | ||
} | ||
}, { | ||
desc: "Replace .toHaveLength(1) with .toBeInTheDocument()", | ||
fix(fixer) { | ||
// Remove any arguments in the matcher | ||
return [...Array.from(matcherArguments).map(argument => fixer.remove(argument)), fixer.replaceText(matcherNode, "toBeInTheDocument")]; | ||
} | ||
}] | ||
}); | ||
} | ||
} | ||
@@ -151,2 +208,7 @@ | ||
const queryNode = (0, _assignmentAst.getAssignmentForIdentifier)(context, node.object.object.arguments[0].name); | ||
// Not an RTL query | ||
if (!queryNode || queryNode.type !== "CallExpression") { | ||
return; | ||
} | ||
const matcherNode = node.property; | ||
@@ -157,3 +219,3 @@ const matcherArguments = node.parent.arguments; | ||
negatedMatcher: true, | ||
queryNode: queryNode && queryNode.callee || queryNode, | ||
queryNode: queryNode.callee, | ||
matcherNode, | ||
@@ -166,3 +228,10 @@ matcherArguments, | ||
[`MemberExpression[object.callee.name=expect][property.name=${alternativeMatchers}][object.arguments.0.type=Identifier]`](node) { | ||
const queryNode = (0, _assignmentAst.getAssignmentForIdentifier)(context, node.object.arguments[0].name); | ||
// Value expression being assigned to the left-hand value | ||
const rightValueNode = (0, _assignmentAst.getAssignmentForIdentifier)(context, node.object.arguments[0].name); | ||
// Not a DTL query | ||
if (!rightValueNode || rightValueNode.type !== "CallExpression") { | ||
return; | ||
} | ||
const queryIdentifierNode = getDTLQueryIdentifierNode(rightValueNode); | ||
const matcherNode = node.property; | ||
@@ -172,3 +241,3 @@ const matcherArguments = node.parent.arguments; | ||
negatedMatcher: false, | ||
queryNode: queryNode && queryNode.callee || queryNode, | ||
queryNode: queryIdentifierNode, | ||
matcherNode, | ||
@@ -185,3 +254,3 @@ matcherArguments | ||
} | ||
const queryNode = arg.type === "AwaitExpression" ? arg.argument.callee : arg.callee; | ||
const queryIdentifierNode = arg.type === "AwaitExpression" ? getDTLQueryIdentifierNode(arg.argument) : getDTLQueryIdentifierNode(arg); | ||
const matcherNode = node.callee.property; | ||
@@ -191,3 +260,3 @@ const matcherArguments = node.arguments; | ||
negatedMatcher: false, | ||
queryNode, | ||
queryNode: queryIdentifierNode, | ||
matcherNode, | ||
@@ -194,0 +263,0 @@ matcherArguments |
{ | ||
"name": "eslint-plugin-jest-dom", | ||
"version": "5.0.1", | ||
"version": "5.0.2", | ||
"description": "ESLint plugin to follow best practices and anticipate common mistakes when writing tests with jest-dom", | ||
@@ -55,7 +55,7 @@ "main": "dist/index.js", | ||
"kcd-scripts": "^12.0.0", | ||
"typescript": "^4.5.3" | ||
"typescript": "^5.1.3" | ||
}, | ||
"peerDependencies": { | ||
"eslint": "^6.8.0 || ^7.0.0 || ^8.0.0", | ||
"@testing-library/dom": "^8.0.0 || ^9.0.0" | ||
"@testing-library/dom": "^8.0.0 || ^9.0.0", | ||
"eslint": "^6.8.0 || ^7.0.0 || ^8.0.0" | ||
}, | ||
@@ -62,0 +62,0 @@ "eslintConfig": { |
@@ -99,17 +99,18 @@ <div align="center"> | ||
✅ Set in the `recommended` configuration.\ | ||
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). | ||
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ | ||
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). | ||
| Name | Description | 💼 | 🔧 | | ||
| :----------------------------------------------------------------------- | :-------------------------------------------------------------------- | :- | :- | | ||
| [prefer-checked](docs/rules/prefer-checked.md) | prefer toBeChecked over checking attributes | ✅ | 🔧 | | ||
| [prefer-empty](docs/rules/prefer-empty.md) | Prefer toBeEmpty over checking innerHTML | ✅ | 🔧 | | ||
| [prefer-enabled-disabled](docs/rules/prefer-enabled-disabled.md) | prefer toBeDisabled or toBeEnabled over checking attributes | ✅ | 🔧 | | ||
| [prefer-focus](docs/rules/prefer-focus.md) | prefer toHaveFocus over checking document.activeElement | ✅ | 🔧 | | ||
| [prefer-in-document](docs/rules/prefer-in-document.md) | Prefer .toBeInTheDocument() for asserting the existence of a DOM node | ✅ | 🔧 | | ||
| [prefer-required](docs/rules/prefer-required.md) | prefer toBeRequired over checking properties | ✅ | 🔧 | | ||
| [prefer-to-have-attribute](docs/rules/prefer-to-have-attribute.md) | prefer toHaveAttribute over checking getAttribute/hasAttribute | ✅ | 🔧 | | ||
| [prefer-to-have-class](docs/rules/prefer-to-have-class.md) | prefer toHaveClass over checking element className | ✅ | 🔧 | | ||
| [prefer-to-have-style](docs/rules/prefer-to-have-style.md) | prefer toHaveStyle over checking element style | ✅ | 🔧 | | ||
| [prefer-to-have-text-content](docs/rules/prefer-to-have-text-content.md) | Prefer toHaveTextContent over checking element.textContent | ✅ | 🔧 | | ||
| [prefer-to-have-value](docs/rules/prefer-to-have-value.md) | prefer toHaveValue over checking element.value | ✅ | 🔧 | | ||
| Name | Description | 💼 | 🔧 | 💡 | | ||
| :----------------------------------------------------------------------- | :-------------------------------------------------------------------- | :- | :- | :- | | ||
| [prefer-checked](docs/rules/prefer-checked.md) | prefer toBeChecked over checking attributes | ✅ | 🔧 | | | ||
| [prefer-empty](docs/rules/prefer-empty.md) | Prefer toBeEmpty over checking innerHTML | ✅ | 🔧 | | | ||
| [prefer-enabled-disabled](docs/rules/prefer-enabled-disabled.md) | prefer toBeDisabled or toBeEnabled over checking attributes | ✅ | 🔧 | | | ||
| [prefer-focus](docs/rules/prefer-focus.md) | prefer toHaveFocus over checking document.activeElement | ✅ | 🔧 | | | ||
| [prefer-in-document](docs/rules/prefer-in-document.md) | Prefer .toBeInTheDocument() for asserting the existence of a DOM node | ✅ | 🔧 | 💡 | | ||
| [prefer-required](docs/rules/prefer-required.md) | prefer toBeRequired over checking properties | ✅ | 🔧 | | | ||
| [prefer-to-have-attribute](docs/rules/prefer-to-have-attribute.md) | prefer toHaveAttribute over checking getAttribute/hasAttribute | ✅ | 🔧 | | | ||
| [prefer-to-have-class](docs/rules/prefer-to-have-class.md) | prefer toHaveClass over checking element className | ✅ | 🔧 | | | ||
| [prefer-to-have-style](docs/rules/prefer-to-have-style.md) | prefer toHaveStyle over checking element style | ✅ | 🔧 | | | ||
| [prefer-to-have-text-content](docs/rules/prefer-to-have-text-content.md) | Prefer toHaveTextContent over checking element.textContent | ✅ | 🔧 | | | ||
| [prefer-to-have-value](docs/rules/prefer-to-have-value.md) | prefer toHaveValue over checking element.value | ✅ | 🔧 | | | ||
@@ -116,0 +117,0 @@ <!-- end auto-generated rules list --> |
82358
1336
205