UNPKG

eslint-plugin-testing-library

Version:

ESLint plugin to follow best practices and anticipate common mistakes when writing tests with Testing Library

1,046 lines (1,030 loc) 168 kB
let __typescript_eslint_utils = require("@typescript-eslint/utils"); let __typescript_eslint_scope_manager = require("@typescript-eslint/scope-manager"); let __typescript_eslint_utils_ast_utils = require("@typescript-eslint/utils/ast-utils"); //#region src/configs/angular.ts var angular_default = { rules: { "testing-library/await-async-events": ["error", { eventModule: "userEvent" }], "testing-library/await-async-queries": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-events": ["error", { eventModules: ["fire-event"] }], "testing-library/no-await-sync-queries": "error", "testing-library/no-container": "error", "testing-library/no-debugging-utils": "warn", "testing-library/no-dom-import": ["error", "angular"], "testing-library/no-global-regexp-flag-in-query": "error", "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-render-in-lifecycle": "error", "testing-library/no-wait-for-multiple-assertions": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/no-wait-for-snapshot": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-presence-queries": "error", "testing-library/prefer-query-by-disappearance": "error", "testing-library/prefer-screen-queries": "error", "testing-library/render-result-naming-convention": "error" } }; //#endregion //#region src/configs/dom.ts var dom_default = { rules: { "testing-library/await-async-events": ["error", { eventModule: "userEvent" }], "testing-library/await-async-queries": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-events": ["error", { eventModules: ["fire-event"] }], "testing-library/no-await-sync-queries": "error", "testing-library/no-global-regexp-flag-in-query": "error", "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-wait-for-multiple-assertions": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/no-wait-for-snapshot": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-presence-queries": "error", "testing-library/prefer-query-by-disappearance": "error", "testing-library/prefer-screen-queries": "error" } }; //#endregion //#region src/configs/marko.ts var marko_default = { rules: { "testing-library/await-async-events": ["error", { eventModule: ["fireEvent", "userEvent"] }], "testing-library/await-async-queries": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-queries": "error", "testing-library/no-container": "error", "testing-library/no-debugging-utils": "warn", "testing-library/no-dom-import": ["error", "marko"], "testing-library/no-global-regexp-flag-in-query": "error", "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-render-in-lifecycle": "error", "testing-library/no-unnecessary-act": "error", "testing-library/no-wait-for-multiple-assertions": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/no-wait-for-snapshot": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-presence-queries": "error", "testing-library/prefer-query-by-disappearance": "error", "testing-library/prefer-screen-queries": "error", "testing-library/render-result-naming-convention": "error" } }; //#endregion //#region src/configs/react.ts var react_default = { rules: { "testing-library/await-async-events": ["error", { eventModule: "userEvent" }], "testing-library/await-async-queries": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-events": ["error", { eventModules: ["fire-event"] }], "testing-library/no-await-sync-queries": "error", "testing-library/no-container": "error", "testing-library/no-debugging-utils": "warn", "testing-library/no-dom-import": ["error", "react"], "testing-library/no-global-regexp-flag-in-query": "error", "testing-library/no-manual-cleanup": "error", "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-render-in-lifecycle": "error", "testing-library/no-unnecessary-act": "error", "testing-library/no-wait-for-multiple-assertions": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/no-wait-for-snapshot": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-presence-queries": "error", "testing-library/prefer-query-by-disappearance": "error", "testing-library/prefer-screen-queries": "error", "testing-library/render-result-naming-convention": "error" } }; //#endregion //#region src/configs/svelte.ts var svelte_default = { rules: { "testing-library/await-async-events": ["error", { eventModule: ["fireEvent", "userEvent"] }], "testing-library/await-async-queries": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-queries": "error", "testing-library/no-container": "error", "testing-library/no-debugging-utils": "warn", "testing-library/no-dom-import": ["error", "svelte"], "testing-library/no-global-regexp-flag-in-query": "error", "testing-library/no-manual-cleanup": "error", "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-render-in-lifecycle": "error", "testing-library/no-wait-for-multiple-assertions": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/no-wait-for-snapshot": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-presence-queries": "error", "testing-library/prefer-query-by-disappearance": "error", "testing-library/prefer-screen-queries": "error", "testing-library/render-result-naming-convention": "error" } }; //#endregion //#region src/configs/vue.ts var vue_default = { rules: { "testing-library/await-async-events": ["error", { eventModule: ["fireEvent", "userEvent"] }], "testing-library/await-async-queries": "error", "testing-library/await-async-utils": "error", "testing-library/no-await-sync-queries": "error", "testing-library/no-container": "error", "testing-library/no-debugging-utils": "warn", "testing-library/no-dom-import": ["error", "vue"], "testing-library/no-global-regexp-flag-in-query": "error", "testing-library/no-manual-cleanup": "error", "testing-library/no-node-access": "error", "testing-library/no-promise-in-fire-event": "error", "testing-library/no-render-in-lifecycle": "error", "testing-library/no-wait-for-multiple-assertions": "error", "testing-library/no-wait-for-side-effects": "error", "testing-library/no-wait-for-snapshot": "error", "testing-library/prefer-find-by": "error", "testing-library/prefer-presence-queries": "error", "testing-library/prefer-query-by-disappearance": "error", "testing-library/prefer-screen-queries": "error", "testing-library/render-result-naming-convention": "error" } }; //#endregion //#region src/configs/index.ts const baseConfigs = { dom: dom_default, angular: angular_default, react: react_default, vue: vue_default, svelte: svelte_default, marko: marko_default }; //#endregion //#region src/utils/compat.ts /* istanbul ignore next */ const getFilename = (context) => { return context.filename ?? context.getFilename(); }; /* istanbul ignore next */ const getSourceCode = (context) => { return context.sourceCode ?? context.getSourceCode(); }; /* istanbul ignore next */ const getScope = (context, node) => { return getSourceCode(context).getScope?.(node) ?? context.getScope(); }; /* istanbul ignore next */ const getDeclaredVariables = (context, node) => { return getSourceCode(context).getDeclaredVariables?.(node) ?? context.getDeclaredVariables(node); }; //#endregion //#region src/node-utils/is-node-of-type.ts const isArrayExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ArrayExpression); const isArrowFunctionExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ArrowFunctionExpression); const isBlockStatement = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.BlockStatement); const isCallExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.CallExpression); const isExpressionStatement = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ExpressionStatement); const isVariableDeclaration = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.VariableDeclaration); const isAssignmentExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.AssignmentExpression); const isChainExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ChainExpression); const isSequenceExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.SequenceExpression); const isImportDeclaration = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ImportDeclaration); const isImportDefaultSpecifier = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ImportDefaultSpecifier); const isTSImportEqualsDeclaration = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.TSImportEqualsDeclaration); const isImportExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ImportExpression); const isImportNamespaceSpecifier = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ImportNamespaceSpecifier); const isImportSpecifier = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ImportSpecifier); const isJSXAttribute = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.JSXAttribute); const isLiteral = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.Literal); const isTemplateLiteral = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.TemplateLiteral); const isMemberExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.MemberExpression); const isNewExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.NewExpression); const isObjectExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression); const isObjectPattern = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ObjectPattern); const isProperty = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.Property); const isReturnStatement = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.ReturnStatement); const isFunctionExpression = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.FunctionExpression); const isFunctionDeclaration = __typescript_eslint_utils.ASTUtils.isNodeOfType(__typescript_eslint_utils.AST_NODE_TYPES.FunctionDeclaration); //#endregion //#region src/node-utils/index.ts const ValidLeftHandSideExpressions = [ __typescript_eslint_utils.AST_NODE_TYPES.CallExpression, __typescript_eslint_utils.AST_NODE_TYPES.ClassExpression, __typescript_eslint_utils.AST_NODE_TYPES.ClassDeclaration, __typescript_eslint_utils.AST_NODE_TYPES.FunctionExpression, __typescript_eslint_utils.AST_NODE_TYPES.Literal, __typescript_eslint_utils.AST_NODE_TYPES.TemplateLiteral, __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression, __typescript_eslint_utils.AST_NODE_TYPES.ArrayExpression, __typescript_eslint_utils.AST_NODE_TYPES.ArrayPattern, __typescript_eslint_utils.AST_NODE_TYPES.ClassExpression, __typescript_eslint_utils.AST_NODE_TYPES.FunctionExpression, __typescript_eslint_utils.AST_NODE_TYPES.Identifier, __typescript_eslint_utils.AST_NODE_TYPES.JSXElement, __typescript_eslint_utils.AST_NODE_TYPES.JSXFragment, __typescript_eslint_utils.AST_NODE_TYPES.JSXOpeningElement, __typescript_eslint_utils.AST_NODE_TYPES.MetaProperty, __typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression, __typescript_eslint_utils.AST_NODE_TYPES.ObjectPattern, __typescript_eslint_utils.AST_NODE_TYPES.Super, __typescript_eslint_utils.AST_NODE_TYPES.ThisExpression, __typescript_eslint_utils.AST_NODE_TYPES.TSNullKeyword, __typescript_eslint_utils.AST_NODE_TYPES.TaggedTemplateExpression, __typescript_eslint_utils.AST_NODE_TYPES.TSNonNullExpression, __typescript_eslint_utils.AST_NODE_TYPES.TSAsExpression, __typescript_eslint_utils.AST_NODE_TYPES.ArrowFunctionExpression ]; /** * Finds the closest CallExpression node for a given node. * @param node * @param shouldRestrictInnerScope - If true, CallExpression must belong to innermost scope of given node */ function findClosestCallExpressionNode(node, shouldRestrictInnerScope = false) { if (isCallExpression(node)) return node; if (!node?.parent) return null; if (shouldRestrictInnerScope && !ValidLeftHandSideExpressions.includes(node.parent.type)) return null; return findClosestCallExpressionNode(node.parent, shouldRestrictInnerScope); } function findClosestVariableDeclaratorNode(node) { if (!node) return null; if (__typescript_eslint_utils.ASTUtils.isVariableDeclarator(node)) return node; return findClosestVariableDeclaratorNode(node.parent); } function findClosestFunctionExpressionNode(node) { if (!node) return null; if (isArrowFunctionExpression(node) || isFunctionExpression(node) || isFunctionDeclaration(node)) return node; return findClosestFunctionExpressionNode(node.parent); } /** * TODO: remove this one in favor of {@link findClosestCallExpressionNode} */ function findClosestCallNode(node, name$1) { if (!node.parent) return null; if (isCallExpression(node) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.callee) && node.callee.name === name$1) return node; else return findClosestCallNode(node.parent, name$1); } function hasThenProperty(node) { return isMemberExpression(node) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.property) && node.property.name === "then"; } function hasPromiseHandlerProperty(node) { return isMemberExpression(node) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.property) && [ "then", "catch", "finally" ].includes(node.property.name); } function hasChainedPromiseHandler(node) { const parent = node.parent; if (isCallExpression(parent) && parent.parent) return hasPromiseHandlerProperty(parent.parent); return !!parent && hasPromiseHandlerProperty(parent); } function isPromiseIdentifier(node) { return __typescript_eslint_utils.ASTUtils.isIdentifier(node) && node.name === "Promise"; } function isPromiseAll(node) { return isMemberExpression(node.callee) && isPromiseIdentifier(node.callee.object) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.callee.property) && node.callee.property.name === "all"; } function isPromiseAllSettled(node) { return isMemberExpression(node.callee) && isPromiseIdentifier(node.callee.object) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.callee.property) && node.callee.property.name === "allSettled"; } /** * Determines whether a given node belongs to handled `Promise.all` or `Promise.allSettled` * array expression. */ function isPromisesArrayResolved(node) { const closestCallExpression = findClosestCallExpressionNode(node, true); if (!closestCallExpression) return false; return !!closestCallExpression.parent && isArrayExpression(closestCallExpression.parent) && isCallExpression(closestCallExpression.parent.parent) && (isPromiseAll(closestCallExpression.parent.parent) || isPromiseAllSettled(closestCallExpression.parent.parent)); } /** * Determines whether an Identifier related to a promise is considered as handled. * * It will be considered as handled if: * - it belongs to the `await` expression * - it belongs to the `Promise.all` method * - it belongs to the `Promise.allSettled` method * - it's chained with the `then`, `catch`, `finally` method * - it's returned from a function * - has `resolves` or `rejects` jest methods * - has `toResolve` or `toReject` jest-extended matchers * - has a jasmine async matcher */ function isPromiseHandled(nodeIdentifier) { const closestCallExpressionNode = findClosestCallExpressionNode(nodeIdentifier, true); return [nodeIdentifier, closestCallExpressionNode == null ? null : getRootExpression(closestCallExpressionNode)].filter((node) => node != null).some((node) => { if (!node.parent) return false; if (__typescript_eslint_utils.ASTUtils.isAwaitExpression(node.parent)) return true; if (isArrowFunctionExpression(node.parent) || isReturnStatement(node.parent)) return true; if (hasClosestExpectHandlesPromise(node.parent)) return true; if (hasChainedPromiseHandler(node)) return true; if (isPromisesArrayResolved(node)) return true; }); } /** * For an expression in a parent that evaluates to the expression or another child returns the parent node recursively. */ function getRootExpression(expression) { const { parent } = expression; if (parent == null) return expression; switch (parent.type) { case __typescript_eslint_utils.AST_NODE_TYPES.ConditionalExpression: return getRootExpression(parent); case __typescript_eslint_utils.AST_NODE_TYPES.LogicalExpression: { let rootExpression; switch (parent.operator) { case "??": case "||": rootExpression = getRootExpression(parent); break; case "&&": rootExpression = parent.right === expression ? getRootExpression(parent) : expression; break; } return rootExpression ?? expression; } case __typescript_eslint_utils.AST_NODE_TYPES.SequenceExpression: return parent.expressions[parent.expressions.length - 1] === expression ? getRootExpression(parent) : expression; case __typescript_eslint_utils.AST_NODE_TYPES.ChainExpression: return getRootExpression(parent); default: return expression; } } function getVariableReferences(context, node) { if (__typescript_eslint_utils.ASTUtils.isVariableDeclarator(node)) return getDeclaredVariables(context, node)[0]?.references?.slice(1) ?? []; return []; } function getInnermostFunctionScope(context, asyncQueryNode) { const innermostScope = __typescript_eslint_utils.ASTUtils.getInnermostScope(getScope(context, asyncQueryNode), asyncQueryNode); if (innermostScope.type === __typescript_eslint_scope_manager.ScopeType.function && __typescript_eslint_utils.ASTUtils.isFunction(innermostScope.block)) return innermostScope; return null; } function getFunctionReturnStatementNode(functionNode) { if (isBlockStatement(functionNode.body)) { const returnStatementNode = functionNode.body.body.find((statement) => isReturnStatement(statement)); if (!returnStatementNode) return null; return returnStatementNode.argument; } else if (functionNode.expression) return functionNode.body; return null; } /** * Gets the property identifier node of a given property node. * * Not to be confused with {@link getDeepestIdentifierNode} * * An example: * Having `const a = rtl.within('foo').getByRole('button')`: * if we call `getPropertyIdentifierNode` with `rtl` property node, * it will return `rtl` identifier node */ function getPropertyIdentifierNode(node) { if (__typescript_eslint_utils.ASTUtils.isIdentifier(node)) return node; if (isMemberExpression(node)) return getPropertyIdentifierNode(node.object); if (isCallExpression(node)) return getPropertyIdentifierNode(node.callee); if (isExpressionStatement(node) || isChainExpression(node)) return getPropertyIdentifierNode(node.expression); if (__typescript_eslint_utils.ASTUtils.isAwaitExpression(node)) return getPropertyIdentifierNode(node.argument); return null; } /** * Gets the deepest identifier node in the expression from a given node. * * Opposite of {@link getReferenceNode} * * An example: * Having `const a = rtl.within('foo').getByRole('button')`: * if we call `getDeepestIdentifierNode` with `rtl` node, * it will return `getByRole` identifier */ function getDeepestIdentifierNode(node) { if (__typescript_eslint_utils.ASTUtils.isIdentifier(node)) return node; if (isChainExpression(node)) return getDeepestIdentifierNode(node.expression); if (isMemberExpression(node) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.property)) return node.property; if (isCallExpression(node)) return getDeepestIdentifierNode(node.callee); if (__typescript_eslint_utils.ASTUtils.isAwaitExpression(node)) return getDeepestIdentifierNode(node.argument); return null; } /** * Gets the farthest node in the expression from a given node. * * Opposite of {@link getDeepestIdentifierNode} * An example: * Having `const a = rtl.within('foo').getByRole('button')`: * if we call `getReferenceNode` with `getByRole` identifier, * it will return `rtl` node */ function getReferenceNode(node) { if (node.parent && (isMemberExpression(node.parent) || isCallExpression(node.parent))) return getReferenceNode(node.parent); return node; } function getFunctionName(node) { return __typescript_eslint_utils.ASTUtils.getFunctionNameWithKind(node).match(/('\w+')/g)?.[0].replace(/'/g, "") ?? ""; } function getImportModuleName(node) { if (isImportDeclaration(node) && typeof node.source.value === "string") return node.source.value; if (isCallExpression(node) && isLiteral(node.arguments[0]) && typeof node.arguments[0].value === "string") return node.arguments[0].value; } /** * Extracts matcher info from MemberExpression node representing an assert. */ function getAssertNodeInfo(node) { const emptyInfo = { matcher: null, isNegated: false }; if (!isCallExpression(node.object) || !__typescript_eslint_utils.ASTUtils.isIdentifier(node.object.callee)) return emptyInfo; if (node.object.callee.name !== "expect") return emptyInfo; let matcher = __typescript_eslint_utils.ASTUtils.getPropertyName(node); const isNegated = matcher === "not"; if (isNegated) matcher = node.parent && isMemberExpression(node.parent) ? __typescript_eslint_utils.ASTUtils.getPropertyName(node.parent) : null; if (!matcher) return emptyInfo; return { matcher, isNegated }; } const matcherNamesHandlePromise = [ "resolves", "rejects", "toResolve", "toReject", "toBeRejected", "toBeRejectedWith", "toBeRejectedWithError", "toBePending", "toBeResolved", "toBeResolvedTo" ]; /** * Determines whether a node belongs to an async assertion that is fulfilled by: * - `resolves` or `rejects` properties * - `toResolve` or `toReject` jest-extended matchers * - jasmine async matchers */ function hasClosestExpectHandlesPromise(node) { if (isCallExpression(node) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.callee) && node.parent && isMemberExpression(node.parent) && ["expect", "expectAsync"].includes(node.callee.name)) { const expectMatcher = node.parent.property; return __typescript_eslint_utils.ASTUtils.isIdentifier(expectMatcher) && matcherNamesHandlePromise.includes(expectMatcher.name); } if (!node.parent) return false; return hasClosestExpectHandlesPromise(node.parent); } /** * Gets the Function node which returns the given Identifier. */ function getInnermostReturningFunction(context, node) { const functionScope = getInnermostFunctionScope(context, node); if (!functionScope) return; const returnStatementNode = getFunctionReturnStatementNode(functionScope.block); if (!returnStatementNode) return; if (getDeepestIdentifierNode(returnStatementNode)?.name !== node.name) return; return functionScope.block; } function hasImportMatch(importNode, identifierName) { if (__typescript_eslint_utils.ASTUtils.isIdentifier(importNode)) return importNode.name === identifierName; return importNode.local.name === identifierName; } function getCallExpressionFromNode(node) { if (isCallExpression(node)) return node; if (isChainExpression(node)) return getCallExpressionFromNode(node.expression); if (__typescript_eslint_utils.ASTUtils.isAwaitExpression(node)) return getCallExpressionFromNode(node.argument); if (isAssignmentExpression(node)) return getCallExpressionFromNode(node.right); return null; } function getStatementCallExpression(statement) { if (isExpressionStatement(statement)) return getCallExpressionFromNode(statement.expression); if (isReturnStatement(statement)) return getCallExpressionFromNode(statement.argument); if (isVariableDeclaration(statement)) for (const declaration of statement.declarations) return getCallExpressionFromNode(declaration.init); return null; } /** * Determines whether a given function node is considered as empty function or not. * * A function is considered empty if its body is empty. * * Note that comments don't affect the check. * * If node given is not a function, `false` will be returned. */ function isEmptyFunction(node) { if (__typescript_eslint_utils.ASTUtils.isFunction(node) && isBlockStatement(node.body)) return node.body.body.length === 0; return false; } /** * Finds the import specifier matching a given name for a given import module node. */ function findImportSpecifier(specifierName, node) { if (isImportDeclaration(node)) { const namedExport = node.specifiers.find((n) => { return isImportSpecifier(n) && __typescript_eslint_utils.ASTUtils.isIdentifier(n.imported) && [n.imported.name, n.local.name].includes(specifierName); }); if (namedExport) return namedExport; return node.specifiers.find((n) => isImportNamespaceSpecifier(n)); } else { if (!__typescript_eslint_utils.ASTUtils.isVariableDeclarator(node.parent)) return; const requireNode = node.parent; if (__typescript_eslint_utils.ASTUtils.isIdentifier(requireNode.id)) return requireNode.id; if (!isObjectPattern(requireNode.id)) return; const property = requireNode.id.properties.find((n) => isProperty(n) && __typescript_eslint_utils.ASTUtils.isIdentifier(n.key) && n.key.name === specifierName); if (!property) return; return property.key; } } //#endregion //#region src/utils/is-testing-library-module.ts const isOfficialTestingLibraryModule = (importSourceName) => [ ...OLD_LIBRARY_MODULES, ...LIBRARY_MODULES, USER_EVENT_MODULE ].includes(importSourceName); const isCustomTestingLibraryModule = (importSourceName, customModuleSetting) => typeof customModuleSetting === "string" && importSourceName.endsWith(customModuleSetting); const isTestingLibraryModule = (importSourceName, customModuleSetting) => isOfficialTestingLibraryModule(importSourceName) || isCustomTestingLibraryModule(importSourceName, customModuleSetting); //#endregion //#region src/node-utils/accessors.ts /** * Checks if the given `node` is a `StringLiteral`. * * If a `value` is provided & the `node` is a `StringLiteral`, * the `value` will be compared to that of the `StringLiteral`. */ const isStringLiteral = (node, value) => isLiteral(node) && typeof node.value === "string" && (value === void 0 || node.value === value); /** * Checks if the given `node` is a `TemplateLiteral`. * * Complex `TemplateLiteral`s are not considered specific, and so will return `false`. * * If a `value` is provided & the `node` is a `TemplateLiteral`, * the `value` will be compared to that of the `TemplateLiteral`. */ const isSimpleTemplateLiteral = (node, value) => isTemplateLiteral(node) && node.quasis.length === 1 && (value === void 0 || node.quasis[0]?.value.raw === value); /** * Checks if the given `node` is a {@link StringNode}. */ const isStringNode = (node, specifics) => isStringLiteral(node, specifics) || isSimpleTemplateLiteral(node, specifics); /** * Gets the value of the given `StringNode`. * * If the `node` is a `TemplateLiteral`, the `raw` value is used; * otherwise, `value` is returned instead. */ const getStringValue = (node) => isSimpleTemplateLiteral(node) ? node.quasis[0].value.raw : node.value; /** * Checks if the given `node` is an `Identifier`. * * If a `name` is provided, & the `node` is an `Identifier`, * the `name` will be compared to that of the `identifier`. */ const isIdentifier$1 = (node, name$1) => __typescript_eslint_utils.ASTUtils.isIdentifier(node) && (name$1 === void 0 || node.name === name$1); /** * Checks if the given `node` is a "supported accessor". * * This means that it's a node can be used to access properties, * and who's "value" can be statically determined. * * `MemberExpression` nodes most commonly contain accessors, * but it's possible for other nodes to contain them. * * If a `value` is provided & the `node` is an `AccessorNode`, * the `value` will be compared to that of the `AccessorNode`. * * Note that `value` here refers to the normalised value. * The property that holds the value is not always called `name`. */ const isSupportedAccessor = (node, value) => isIdentifier$1(node, value) || isStringNode(node, value); /** * Gets the value of the given `AccessorNode`, * account for the different node types. */ const getAccessorValue = (accessor) => accessor.type === __typescript_eslint_utils.AST_NODE_TYPES.Identifier ? accessor.name : getStringValue(accessor); //#endregion //#region src/utils/resolve-to-testing-library-fn.ts const describeImportDefAsImport = (def) => { if (isTSImportEqualsDeclaration(def.parent)) return null; if (isImportDefaultSpecifier(def.node)) return { source: def.parent.source.value, imported: null, local: def.node.local.name }; if (!isImportSpecifier(def.node)) return null; if (def.parent.importKind === "type") return null; return { source: def.parent.source.value, imported: "name" in def.node.imported ? def.node.imported.name : def.node.imported.value, local: def.node.local.name }; }; const describeVariableDefAsImport = (def) => { if (!def.node.init) return null; const sourceNode = isCallExpression(def.node.init) && isIdentifier$1(def.node.init.callee, "require") ? def.node.init.arguments[0] : __typescript_eslint_utils.ASTUtils.isAwaitExpression(def.node.init) && isImportExpression(def.node.init.argument) ? def.node.init.argument.source : null; if (!sourceNode || !isStringNode(sourceNode)) return null; if (!isProperty(def.name.parent)) return null; if (!isSupportedAccessor(def.name.parent.key)) return null; return { source: getStringValue(sourceNode), imported: getAccessorValue(def.name.parent.key), local: def.name.name }; }; const describePossibleImportDef = (def) => { if (def.type === __typescript_eslint_scope_manager.DefinitionType.Variable) return describeVariableDefAsImport(def); if (def.type === __typescript_eslint_scope_manager.DefinitionType.ImportBinding) return describeImportDefAsImport(def); return null; }; const resolveScope = (scope, identifier) => { let currentScope = scope; while (currentScope !== null) { const ref = currentScope.set.get(identifier); if (ref && ref.defs.length > 0) { const def = ref.defs[ref.defs.length - 1]; const importDetails = def ? describePossibleImportDef(def) : null; if (importDetails?.local === identifier) return importDetails; return "local"; } currentScope = currentScope.upper; } return null; }; const joinChains = (a, b) => a && b ? [...a, ...b] : null; const getNodeChain = (node) => { if (isSupportedAccessor(node)) return [node]; switch (node.type) { case __typescript_eslint_utils.AST_NODE_TYPES.MemberExpression: return joinChains(getNodeChain(node.object), getNodeChain(node.property)); case __typescript_eslint_utils.AST_NODE_TYPES.CallExpression: return getNodeChain(node.callee); } return null; }; const resolveToTestingLibraryFn = (node, context) => { const chain = getNodeChain(node); if (!chain?.length) return null; const identifier = chain[0]; if (!identifier) return null; const maybeImport = resolveScope(context.sourceCode.getScope(identifier), getAccessorValue(identifier)); if (maybeImport === "local" || maybeImport === null) return null; const customModuleSetting = context.settings["testing-library/utils-module"]; if (isTestingLibraryModule(maybeImport.source, customModuleSetting)) return { original: maybeImport.imported, local: maybeImport.local }; return null; }; //#endregion //#region src/utils/index.ts const combineQueries = (variants, methods) => { const combinedQueries = []; variants.forEach((variant) => { const variantPrefix = variant.replace("By", ""); methods.forEach((method) => { combinedQueries.push(`${variantPrefix}${method}`); }); }); return combinedQueries; }; const getDocsUrl = (ruleName) => `https://github.com/testing-library/eslint-plugin-testing-library/tree/main/docs/rules/${ruleName}.md`; const LIBRARY_MODULES = [ "@testing-library/dom", "@testing-library/angular", "@testing-library/react", "@testing-library/preact", "@testing-library/vue", "@testing-library/svelte", "@marko/testing-library" ]; const USER_EVENT_MODULE = "@testing-library/user-event"; const OLD_LIBRARY_MODULES = [ "dom-testing-library", "vue-testing-library", "react-testing-library" ]; const SYNC_QUERIES_VARIANTS = [ "getBy", "getAllBy", "queryBy", "queryAllBy" ]; const ASYNC_QUERIES_VARIANTS = ["findBy", "findAllBy"]; const ALL_QUERIES_VARIANTS = [...SYNC_QUERIES_VARIANTS, ...ASYNC_QUERIES_VARIANTS]; const ALL_QUERIES_METHODS = [ "ByLabelText", "ByPlaceholderText", "ByText", "ByAltText", "ByTitle", "ByDisplayValue", "ByRole", "ByTestId" ]; const SYNC_QUERIES_COMBINATIONS = combineQueries(SYNC_QUERIES_VARIANTS, ALL_QUERIES_METHODS); const ASYNC_QUERIES_COMBINATIONS = combineQueries(ASYNC_QUERIES_VARIANTS, ALL_QUERIES_METHODS); const ALL_QUERIES_COMBINATIONS = [...SYNC_QUERIES_COMBINATIONS, ...ASYNC_QUERIES_COMBINATIONS]; const ASYNC_UTILS = ["waitFor", "waitForElementToBeRemoved"]; const DEBUG_UTILS = [ "debug", "logTestingPlaygroundURL", "prettyDOM", "logRoles", "logDOM", "prettyFormat" ]; const EVENTS_SIMULATORS = ["fireEvent", "userEvent"]; const TESTING_FRAMEWORK_SETUP_HOOKS = ["beforeEach", "beforeAll"]; const PROPERTIES_RETURNING_NODES = [ "activeElement", "children", "childElementCount", "firstChild", "firstElementChild", "fullscreenElement", "lastChild", "lastElementChild", "nextElementSibling", "nextSibling", "parentElement", "parentNode", "pointerLockElement", "previousElementSibling", "previousSibling", "rootNode", "scripts" ]; const METHODS_RETURNING_NODES = [ "closest", "getElementById", "getElementsByClassName", "getElementsByName", "getElementsByTagName", "getElementsByTagNameNS", "querySelector", "querySelectorAll" ]; const EVENT_HANDLER_METHODS = [ "click", "select", "submit" ]; const ALL_RETURNING_NODES = [...PROPERTIES_RETURNING_NODES, ...METHODS_RETURNING_NODES]; const PRESENCE_MATCHERS = [ "toBeOnTheScreen", "toBeInTheDocument", "toBeTruthy", "toBeDefined" ]; const ABSENCE_MATCHERS = ["toBeNull", "toBeFalsy"]; //#endregion //#region src/create-testing-library-rule/detect-testing-library-utils.ts const SETTING_OPTION_OFF = "off"; const REACT_DOM_TEST_UTILS_PACKAGE = "react-dom/test-utils"; const FIRE_EVENT_NAME$1 = "fireEvent"; const CREATE_EVENT_NAME = "createEvent"; const USER_EVENT_NAME$2 = "userEvent"; const RENDER_NAME = "render"; /** * Enhances a given rule `create` with helpers to detect Testing Library utils. */ function detectTestingLibraryUtils(ruleCreate, { skipRuleReportingCheck = false } = {}) { return (context, optionsWithDefault) => { const importedTestingLibraryNodes = []; let importedCustomModuleNode = null; let importedUserEventLibraryNode = null; let importedReactDomTestUtilsNode = null; const customModuleSetting = context.settings["testing-library/utils-module"]; const customRendersSetting = context.settings["testing-library/custom-renders"]; const customQueriesSetting = context.settings["testing-library/custom-queries"]; /** * Small method to extract common checks to determine whether a node is * related to Testing Library or not. * * To determine whether a node is a valid Testing Library util, there are * two conditions to match: * - it's named in a particular way (decided by given callback) * - it's imported from valid Testing Library module (depends on aggressive * reporting) */ function isPotentialTestingLibraryFunction(node, isPotentialFunctionCallback) { if (!node) return false; const referenceNodeIdentifier = getPropertyIdentifierNode(getReferenceNode(node)); if (!referenceNodeIdentifier) return false; const importedUtilSpecifier = getTestingLibraryImportedUtilSpecifier(referenceNodeIdentifier); const originalNodeName = isImportSpecifier(importedUtilSpecifier) && __typescript_eslint_utils.ASTUtils.isIdentifier(importedUtilSpecifier.imported) && importedUtilSpecifier.local.name !== importedUtilSpecifier.imported.name ? importedUtilSpecifier.imported.name : void 0; if (!isPotentialFunctionCallback(node.name, originalNodeName)) return false; if (isAggressiveModuleReportingEnabled()) return true; return isNodeComingFromTestingLibrary(referenceNodeIdentifier); } /** * Determines whether aggressive module reporting is enabled or not. * * This aggressive reporting mechanism is considered as enabled when custom * module is not set, so we need to assume everything matching Testing * Library utils is related to Testing Library no matter from where module * they are coming from. Otherwise, this aggressive reporting mechanism is * opted-out in favour to report only those utils coming from Testing * Library package or custom module set up on settings. */ const isAggressiveModuleReportingEnabled = () => !customModuleSetting; /** * Determines whether aggressive render reporting is enabled or not. * * This aggressive reporting mechanism is considered as enabled when custom * renders are not set, so we need to assume every method containing * "render" is a valid Testing Library `render`. Otherwise, this aggressive * reporting mechanism is opted-out in favour to report only `render` or * names set up on custom renders setting. */ const isAggressiveRenderReportingEnabled = () => { const isSwitchedOff = customRendersSetting === SETTING_OPTION_OFF; const hasCustomOptions = Array.isArray(customRendersSetting) && customRendersSetting.length > 0; return !isSwitchedOff && !hasCustomOptions; }; /** * Determines whether Aggressive Reporting for queries is enabled or not. * * This Aggressive Reporting mechanism is considered as enabled when custom-queries setting is not set, * so the plugin needs to report both built-in and custom queries. * Otherwise, this Aggressive Reporting mechanism is opted-out in favour of reporting only built-in queries + those * indicated in custom-queries setting. */ const isAggressiveQueryReportingEnabled = () => { const isSwitchedOff = customQueriesSetting === SETTING_OPTION_OFF; const hasCustomOptions = Array.isArray(customQueriesSetting) && customQueriesSetting.length > 0; return !isSwitchedOff && !hasCustomOptions; }; const getCustomModule = () => { if (!isAggressiveModuleReportingEnabled() && customModuleSetting !== SETTING_OPTION_OFF) return customModuleSetting; }; const getCustomRenders = () => { if (!isAggressiveRenderReportingEnabled() && customRendersSetting !== SETTING_OPTION_OFF) return customRendersSetting; return []; }; const getCustomQueries = () => { if (!isAggressiveQueryReportingEnabled() && customQueriesSetting !== SETTING_OPTION_OFF) return customQueriesSetting; return []; }; const getTestingLibraryImportNode = () => { return importedTestingLibraryNodes[0] ?? null; }; const getAllTestingLibraryImportNodes = () => { return importedTestingLibraryNodes; }; const getCustomModuleImportNode = () => { return importedCustomModuleNode; }; const getTestingLibraryImportName = () => { return getImportModuleName(importedTestingLibraryNodes[0]); }; const getCustomModuleImportName = () => { return getImportModuleName(importedCustomModuleNode); }; /** * Determines whether Testing Library utils are imported or not for * current file being analyzed. * * By default, it is ALWAYS considered as imported. This is what we call * "aggressive reporting" so we don't miss TL utils reexported from * custom modules. * * However, there is a setting to customize the module where TL utils can * be imported from: "testing-library/utils-module". If this setting is enabled, * then this method will return `true` ONLY IF a testing-library package * or custom module are imported. */ const isTestingLibraryImported = (isStrict = false) => { const isSomeModuleImported = importedTestingLibraryNodes.length !== 0 || !!importedCustomModuleNode; return !isStrict && isAggressiveModuleReportingEnabled() || isSomeModuleImported; }; /** * Determines whether a given node is a reportable query, * either a built-in or a custom one. * * Depending on Aggressive Query Reporting setting, custom queries will be * reportable or not. */ const isQuery = (node) => { if (!/^(get|query|find)(All)?By.+$/.test(node.name)) return false; if (isAggressiveQueryReportingEnabled()) return true; const customQueries = getCustomQueries(); const isBuiltInQuery$1 = ALL_QUERIES_COMBINATIONS.includes(node.name); const isReportableCustomQuery = customQueries.some((pattern) => new RegExp(pattern).test(node.name)); return isBuiltInQuery$1 || isReportableCustomQuery; }; /** * Determines whether a given node is `get*` query variant or not. */ const isGetQueryVariant = (node) => { return isQuery(node) && node.name.startsWith("get"); }; /** * Determines whether a given node is `query*` query variant or not. */ const isQueryQueryVariant = (node) => { return isQuery(node) && node.name.startsWith("query"); }; /** * Determines whether a given node is `find*` query variant or not. */ const isFindQueryVariant = (node) => { return isQuery(node) && node.name.startsWith("find"); }; /** * Determines whether a given node is sync query or not. */ const isSyncQuery = (node) => { return isGetQueryVariant(node) || isQueryQueryVariant(node); }; /** * Determines whether a given node is async query or not. */ const isAsyncQuery = (node) => { return isFindQueryVariant(node); }; const isCustomQuery = (node) => { return isQuery(node) && !ALL_QUERIES_COMBINATIONS.includes(node.name); }; const isBuiltInQuery = (node) => { return isQuery(node) && ALL_QUERIES_COMBINATIONS.includes(node.name); }; /** * Determines whether a given node is a valid async util or not. * * A node will be interpreted as a valid async util based on two conditions: * the name matches with some Testing Library async util, and the node is * coming from Testing Library module. * * The latter depends on Aggressive module reporting: * if enabled, then it doesn't matter from where the given node was imported * from as it will be considered part of Testing Library. * Otherwise, it means `custom-module` has been set up, so only those nodes * coming from Testing Library will be considered as valid. */ const isAsyncUtil = (node, validNames = ASYNC_UTILS) => { return isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => { return validNames.includes(identifierNodeName) || !!originalNodeName && validNames.includes(originalNodeName); }); }; /** * Determines whether a given node is fireEvent util itself or not. * * Not to be confused with {@link isFireEventMethod} */ const isFireEventUtil = (node) => { return isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => { return [identifierNodeName, originalNodeName].includes("fireEvent"); }); }; /** * Determines whether a given node is userEvent util itself or not. * * Not to be confused with {@link isUserEventMethod} */ const isUserEventUtil = (node) => { const userEvent = findImportedUserEventSpecifier(); let userEventName; if (userEvent) userEventName = userEvent.name; else if (isAggressiveModuleReportingEnabled()) userEventName = USER_EVENT_NAME$2; if (!userEventName) return false; return node.name === userEventName; }; /** * Determines whether a given node is fireEvent method or not */ const isFireEventMethod = (node) => { const fireEventUtil = findImportedTestingLibraryUtilSpecifier(FIRE_EVENT_NAME$1); let fireEventUtilName; if (fireEventUtil) fireEventUtilName = __typescript_eslint_utils.ASTUtils.isIdentifier(fireEventUtil) ? fireEventUtil.name : fireEventUtil.local.name; else if (isAggressiveModuleReportingEnabled()) fireEventUtilName = FIRE_EVENT_NAME$1; if (!fireEventUtilName) return false; const parentMemberExpression = node.parent && isMemberExpression(node.parent) ? node.parent : void 0; const parentCallExpression = node.parent && isCallExpression(node.parent) ? node.parent : void 0; if (!parentMemberExpression && !parentCallExpression) return false; if (parentCallExpression) return [fireEventUtilName, FIRE_EVENT_NAME$1].includes(node.name); const definedParentMemberExpression = parentMemberExpression; const regularCall = __typescript_eslint_utils.ASTUtils.isIdentifier(definedParentMemberExpression.object) && isCallExpression(definedParentMemberExpression.parent) && definedParentMemberExpression.object.name === fireEventUtilName && node.name !== FIRE_EVENT_NAME$1 && node.name !== fireEventUtilName; const wildcardCall = isMemberExpression(definedParentMemberExpression.object) && __typescript_eslint_utils.ASTUtils.isIdentifier(definedParentMemberExpression.object.object) && definedParentMemberExpression.object.object.name === fireEventUtilName && __typescript_eslint_utils.ASTUtils.isIdentifier(definedParentMemberExpression.object.property) && definedParentMemberExpression.object.property.name === FIRE_EVENT_NAME$1 && node.name !== FIRE_EVENT_NAME$1 && node.name !== fireEventUtilName; const wildcardCallWithCallExpression = __typescript_eslint_utils.ASTUtils.isIdentifier(definedParentMemberExpression.object) && definedParentMemberExpression.object.name === fireEventUtilName && __typescript_eslint_utils.ASTUtils.isIdentifier(definedParentMemberExpression.property) && definedParentMemberExpression.property.name === FIRE_EVENT_NAME$1 && !isMemberExpression(definedParentMemberExpression.parent) && node.name === FIRE_EVENT_NAME$1 && node.name !== fireEventUtilName; return regularCall || wildcardCall || wildcardCallWithCallExpression; }; const isUserEventMethod = (node, userEventSetupVars) => { const userEvent = findImportedUserEventSpecifier(); let userEventName; if (userEvent) userEventName = userEvent.name; else if (isAggressiveModuleReportingEnabled()) userEventName = USER_EVENT_NAME$2; const parentMemberExpression = node.parent && isMemberExpression(node.parent) ? node.parent : void 0; if (!parentMemberExpression) return false; if (userEventName && [userEventName, USER_EVENT_NAME$2].includes(node.name) || __typescript_eslint_utils.ASTUtils.isIdentifier(parentMemberExpression.object) && parentMemberExpression.object.name === node.name) return false; if (userEventName && __typescript_eslint_utils.ASTUtils.isIdentifier(parentMemberExpression.object) && parentMemberExpression.object.name === userEventName) return true; if (userEventSetupVars && __typescript_eslint_utils.ASTUtils.isIdentifier(parentMemberExpression.object) && userEventSetupVars.has(parentMemberExpression.object.name)) return true; return false; }; /** * Determines whether a given node is a valid render util or not. * * A node will be interpreted as a valid render based on two conditions: * the name matches with a valid "render" option, and the node is coming * from Testing Library module. This depends on: * * - Aggressive render reporting: if enabled, then every node name * containing "render" will be assumed as Testing Library render util. * Otherwise, it means `custom-modules` has been set up, so only those nodes * named as "render" or some of the `custom-modules` options will be * considered as Testing Library render util. * - Aggressive module reporting: if enabled, then it doesn't matter from * where the given node was imported from as it will be considered part of * Testing Library. Otherwise, it means `custom-module` has been set up, so * only those nodes coming from Testing Library will be considered as valid. */ const isRenderUtil = (node) => isPotentialTestingLibraryFunction(node, (identifierNodeName, originalNodeName) => { if (isAggressiveRenderReportingEnabled()) return identifierNodeName.toLowerCase().includes(RENDER_NAME); return [RENDER_NAME, ...getCustomRenders()].some((validRenderName) => validRenderName === identifierNodeName || Boolean(originalNodeName) && validRenderName === originalNodeName); }); const isCreateEventUtil = (node) => { const isCreateEventCallback = (identifierNodeName, originalNodeName) => [identifierNodeName, originalNodeName].includes(CREATE_EVENT_NAME); if (isCallExpression(node) && isMemberExpression(node.callee) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.callee.object)) return isPotentialTestingLibraryFunction(node.callee.object, isCreateEventCallback); if (isCallExpression(node) && isMemberExpression(node.callee) && isMemberExpression(node.callee.object) && __typescript_eslint_utils.ASTUtils.isIdentifier(node.callee.object.property)) return isPotentialTestingLibraryFunction(node.callee.object.property, isCreateEventCallback); return isPotentialTestingLibraryFunction(getDeepestIdentifierNode(node), isCreateEventCallback); }; const isRenderVariableDeclarator