eslint-plugin-jest-dom
Advanced tools
@@ -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 |
+4
-4
| { | ||
| "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": { |
+15
-14
@@ -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
3.33%1336
5.2%205
0.49%