This commit is contained in:
szabomarton
2025-01-28 11:38:27 +01:00
parent 9c5ca86086
commit 7f4a15b9c3
36841 changed files with 4032468 additions and 1 deletions

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'await-async-query';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Enforce promises from async queries to be handled',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
awaitAsyncQuery: 'promise returned from `{{ name }}` query must be handled',
asyncQueryWrapper: 'promise returned from `{{ name }}` wrapper over async query must be handled',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const functionWrappersNames = [];
function detectAsyncQueryWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
functionWrappersNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
CallExpression(node) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!identifierNode) {
return;
}
if (helpers.isAsyncQuery(identifierNode)) {
detectAsyncQueryWrapper(identifierNode);
const closestCallExpressionNode = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!(closestCallExpressionNode === null || closestCallExpressionNode === void 0 ? void 0 : closestCallExpressionNode.parent)) {
return;
}
const references = (0, node_utils_1.getVariableReferences)(context, closestCallExpressionNode.parent);
if (references.length === 0) {
if (!(0, node_utils_1.isPromiseHandled)(identifierNode)) {
context.report({
node: identifierNode,
messageId: 'awaitAsyncQuery',
data: { name: identifierNode.name },
});
return;
}
}
for (const reference of references) {
if (utils_1.ASTUtils.isIdentifier(reference.identifier) &&
!(0, node_utils_1.isPromiseHandled)(reference.identifier)) {
context.report({
node: identifierNode,
messageId: 'awaitAsyncQuery',
data: { name: identifierNode.name },
});
return;
}
}
}
else if (functionWrappersNames.includes(identifierNode.name) &&
!(0, node_utils_1.isPromiseHandled)(identifierNode)) {
context.report({
node: identifierNode,
messageId: 'asyncQueryWrapper',
data: { name: identifierNode.name },
});
}
},
};
},
});

View File

@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'await-async-utils';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Enforce promises from async utils to be awaited properly',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
awaitAsyncUtil: 'Promise returned from `{{ name }}` must be handled',
asyncUtilWrapper: 'Promise returned from {{ name }} wrapper over async util must be handled',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const functionWrappersNames = [];
function detectAsyncUtilWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (!innerFunction) {
return;
}
const functionName = (0, node_utils_1.getFunctionName)(innerFunction);
if (functionName.length === 0) {
return;
}
functionWrappersNames.push(functionName);
}
function detectDestructuredAsyncUtilWrapperAliases(node) {
for (const property of node.properties) {
if (!(0, node_utils_1.isProperty)(property)) {
continue;
}
if (!utils_1.ASTUtils.isIdentifier(property.key) ||
!utils_1.ASTUtils.isIdentifier(property.value)) {
continue;
}
if (functionWrappersNames.includes(property.key.name)) {
const isDestructuredAsyncWrapperPropertyRenamed = property.key.name !== property.value.name;
if (isDestructuredAsyncWrapperPropertyRenamed) {
functionWrappersNames.push(property.value.name);
}
}
}
}
const getMessageId = (node) => {
if (helpers.isAsyncUtil(node)) {
return 'awaitAsyncUtil';
}
return 'asyncUtilWrapper';
};
return {
VariableDeclarator(node) {
var _a, _b;
if ((0, node_utils_1.isObjectPattern)(node.id)) {
detectDestructuredAsyncUtilWrapperAliases(node.id);
return;
}
const isAssigningKnownAsyncFunctionWrapper = utils_1.ASTUtils.isIdentifier(node.id) &&
node.init !== null &&
functionWrappersNames.includes((_b = (_a = (0, node_utils_1.getDeepestIdentifierNode)(node.init)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '');
if (isAssigningKnownAsyncFunctionWrapper) {
functionWrappersNames.push(node.id.name);
}
},
'CallExpression Identifier'(node) {
const isAsyncUtilOrKnownAliasAroundIt = helpers.isAsyncUtil(node) ||
functionWrappersNames.includes(node.name);
if (!isAsyncUtilOrKnownAliasAroundIt) {
return;
}
if (helpers.isAsyncUtil(node)) {
detectAsyncUtilWrapper(node);
}
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!(closestCallExpression === null || closestCallExpression === void 0 ? void 0 : closestCallExpression.parent)) {
return;
}
const references = (0, node_utils_1.getVariableReferences)(context, closestCallExpression.parent);
if (references.length === 0) {
if (!(0, node_utils_1.isPromiseHandled)(node)) {
context.report({
node,
messageId: getMessageId(node),
data: {
name: node.name,
},
});
}
}
else {
for (const reference of references) {
const referenceNode = reference.identifier;
if (!(0, node_utils_1.isPromiseHandled)(referenceNode)) {
context.report({
node,
messageId: getMessageId(node),
data: {
name: node.name,
},
});
return;
}
}
}
},
};
},
});

View File

@@ -0,0 +1,76 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'await-fire-event';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Enforce promises from `fireEvent` methods to be handled',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: 'error',
marko: 'error',
},
},
messages: {
awaitFireEvent: 'Promise returned from `fireEvent.{{ name }}` must be handled',
fireEventWrapper: 'Promise returned from `{{ name }}` wrapper over fire event method must be handled',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const functionWrappersNames = [];
function reportUnhandledNode(node, closestCallExpressionNode, messageId = 'awaitFireEvent') {
if (!(0, node_utils_1.isPromiseHandled)(node)) {
context.report({
node: closestCallExpressionNode.callee,
messageId,
data: { name: node.name },
});
}
}
function detectFireEventMethodWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
functionWrappersNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
'CallExpression Identifier'(node) {
if (helpers.isFireEventMethod(node)) {
detectFireEventMethodWrapper(node);
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!(closestCallExpression === null || closestCallExpression === void 0 ? void 0 : closestCallExpression.parent)) {
return;
}
const references = (0, node_utils_1.getVariableReferences)(context, closestCallExpression.parent);
if (references.length === 0) {
reportUnhandledNode(node, closestCallExpression);
}
else {
for (const reference of references) {
if (utils_1.ASTUtils.isIdentifier(reference.identifier)) {
reportUnhandledNode(reference.identifier, closestCallExpression);
}
}
}
}
else if (functionWrappersNames.includes(node.name)) {
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!closestCallExpression) {
return;
}
reportUnhandledNode(node, closestCallExpression, 'fireEventWrapper');
}
},
};
},
});

View File

@@ -0,0 +1,129 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'consistent-data-testid';
const FILENAME_PLACEHOLDER = '{fileName}';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Ensures consistent usage of `data-testid`',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
consistentDataTestId: '`{{attr}}` "{{value}}" should match `{{regex}}`',
consistentDataTestIdCustomMessage: '`{{message}}`',
},
schema: [
{
type: 'object',
default: {},
additionalProperties: false,
required: ['testIdPattern'],
properties: {
testIdPattern: {
type: 'string',
},
testIdAttribute: {
default: 'data-testid',
oneOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
customMessage: {
default: undefined,
type: 'string',
},
},
},
],
},
defaultOptions: [
{
testIdPattern: '',
testIdAttribute: 'data-testid',
customMessage: undefined,
},
],
detectionOptions: {
skipRuleReportingCheck: true,
},
create: (context, [options]) => {
const { getFilename } = context;
const { testIdPattern, testIdAttribute: attr, customMessage } = options;
function getFileNameData() {
var _a;
const splitPath = getFilename().split('/');
const fileNameWithExtension = (_a = splitPath.pop()) !== null && _a !== void 0 ? _a : '';
if (fileNameWithExtension.includes('[') ||
fileNameWithExtension.includes(']')) {
return { fileName: undefined };
}
const parent = splitPath.pop();
const fileName = fileNameWithExtension.split('.').shift();
return {
fileName: fileName === 'index' ? parent : fileName,
};
}
function getTestIdValidator(fileName) {
return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName));
}
function isTestIdAttribute(name) {
var _a;
if (typeof attr === 'string') {
return attr === name;
}
else {
return (_a = attr === null || attr === void 0 ? void 0 : attr.includes(name)) !== null && _a !== void 0 ? _a : false;
}
}
function getErrorMessageId() {
if (customMessage === undefined) {
return 'consistentDataTestId';
}
return 'consistentDataTestIdCustomMessage';
}
return {
JSXIdentifier: (node) => {
if (!node.parent ||
!(0, node_utils_1.isJSXAttribute)(node.parent) ||
!(0, node_utils_1.isLiteral)(node.parent.value) ||
!isTestIdAttribute(node.name)) {
return;
}
const value = node.parent.value.value;
const { fileName } = getFileNameData();
const regex = getTestIdValidator(fileName !== null && fileName !== void 0 ? fileName : '');
if (value && typeof value === 'string' && !regex.test(value)) {
context.report({
node,
messageId: getErrorMessageId(),
data: {
attr: node.name,
value,
regex,
message: customMessage,
},
});
}
},
};
},
});

View File

@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
const path_1 = require("path");
const utils_1 = require("../utils");
const rulesDir = __dirname;
const excludedFiles = ['index'];
exports.default = (0, fs_1.readdirSync)(rulesDir)
.map((rule) => (0, path_1.parse)(rule).name)
.filter((ruleName) => !excludedFiles.includes(ruleName))
.reduce((allRules, ruleName) => (Object.assign(Object.assign({}, allRules), { [ruleName]: (0, utils_1.importDefault)((0, path_1.join)(rulesDir, ruleName)) })), {});

View File

@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const USER_EVENT_ASYNC_EXCEPTIONS = ['type', 'keyboard'];
const VALID_EVENT_MODULES = ['fire-event', 'user-event'];
exports.RULE_NAME = 'no-await-sync-events';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow unnecessary `await` for sync events',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
noAwaitSyncEvents: '`{{ name }}` is sync and does not need `await` operator',
},
schema: [
{
type: 'object',
properties: {
eventModules: {
type: 'array',
minItems: 1,
items: {
enum: VALID_EVENT_MODULES,
},
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ eventModules: VALID_EVENT_MODULES }],
create(context, [options], helpers) {
const { eventModules = VALID_EVENT_MODULES } = options;
let hasDelayDeclarationOrAssignmentGTZero;
return {
VariableDeclaration(node) {
hasDelayDeclarationOrAssignmentGTZero = node.declarations.some((property) => utils_1.ASTUtils.isIdentifier(property.id) &&
property.id.name === 'delay' &&
(0, node_utils_1.isLiteral)(property.init) &&
property.init.value &&
property.init.value > 0);
},
AssignmentExpression(node) {
if (utils_1.ASTUtils.isIdentifier(node.left) &&
node.left.name === 'delay' &&
(0, node_utils_1.isLiteral)(node.right) &&
node.right.value !== null) {
hasDelayDeclarationOrAssignmentGTZero = node.right.value > 0;
}
},
'AwaitExpression > CallExpression'(node) {
var _a;
const simulateEventFunctionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!simulateEventFunctionIdentifier) {
return;
}
const isUserEventMethod = helpers.isUserEventMethod(simulateEventFunctionIdentifier);
const isFireEventMethod = helpers.isFireEventMethod(simulateEventFunctionIdentifier);
const isSimulateEventMethod = isUserEventMethod || isFireEventMethod;
if (!isSimulateEventMethod) {
return;
}
if (isFireEventMethod && !eventModules.includes('fire-event')) {
return;
}
if (isUserEventMethod && !eventModules.includes('user-event')) {
return;
}
const lastArg = node.arguments[node.arguments.length - 1];
const hasDelayProperty = (0, node_utils_1.isObjectExpression)(lastArg) &&
lastArg.properties.some((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'delay');
const hasDelayLiteralGTZero = (0, node_utils_1.isObjectExpression)(lastArg) &&
lastArg.properties.some((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'delay' &&
(0, node_utils_1.isLiteral)(property.value) &&
!!property.value.value &&
property.value.value > 0);
const simulateEventFunctionName = simulateEventFunctionIdentifier.name;
if (USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) &&
hasDelayProperty &&
(hasDelayDeclarationOrAssignmentGTZero || hasDelayLiteralGTZero)) {
return;
}
const eventModuleName = (_a = (0, node_utils_1.getPropertyIdentifierNode)(node)) === null || _a === void 0 ? void 0 : _a.name;
const eventFullName = eventModuleName
? `${eventModuleName}.${simulateEventFunctionName}`
: simulateEventFunctionName;
context.report({
node,
messageId: 'noAwaitSyncEvents',
data: {
name: eventFullName,
},
});
},
};
},
});

View File

@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-await-sync-query';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow unnecessary `await` for sync queries',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noAwaitSyncQuery: '`{{ name }}` query is sync so it does not need to be awaited',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
return {
'AwaitExpression > CallExpression'(node) {
const deepestIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!deepestIdentifierNode) {
return;
}
if (helpers.isSyncQuery(deepestIdentifierNode)) {
context.report({
node: deepestIdentifierNode,
messageId: 'noAwaitSyncQuery',
data: {
name: deepestIdentifierNode.name,
},
});
}
},
};
},
});

View File

@@ -0,0 +1,123 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-container';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `container` methods',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noContainer: 'Avoid using container methods. Prefer using the methods from Testing Library, such as "getByRole()"',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const destructuredContainerPropNames = [];
const renderWrapperNames = [];
let renderResultVarName = null;
let containerName = null;
let containerCallsMethod = false;
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
function showErrorIfChainedContainerMethod(innerNode) {
if ((0, node_utils_1.isMemberExpression)(innerNode)) {
if (utils_1.ASTUtils.isIdentifier(innerNode.object)) {
const isContainerName = innerNode.object.name === containerName;
if (isContainerName) {
context.report({
node: innerNode,
messageId: 'noContainer',
});
return;
}
const isRenderWrapper = innerNode.object.name === renderResultVarName;
containerCallsMethod =
utils_1.ASTUtils.isIdentifier(innerNode.property) &&
innerNode.property.name === 'container' &&
isRenderWrapper;
if (containerCallsMethod) {
context.report({
node: innerNode.property,
messageId: 'noContainer',
});
return;
}
}
showErrorIfChainedContainerMethod(innerNode.object);
}
}
return {
CallExpression(node) {
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
if (helpers.isRenderUtil(callExpressionIdentifier)) {
detectRenderWrapper(callExpressionIdentifier);
}
if ((0, node_utils_1.isMemberExpression)(node.callee)) {
showErrorIfChainedContainerMethod(node.callee);
}
else if (utils_1.ASTUtils.isIdentifier(node.callee) &&
destructuredContainerPropNames.includes(node.callee.name)) {
context.report({
node,
messageId: 'noContainer',
});
}
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return;
}
const isRenderWrapperVariableDeclarator = renderWrapperNames.includes(initIdentifierNode.name);
if (!helpers.isRenderVariableDeclarator(node) &&
!isRenderWrapperVariableDeclarator) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
const containerIndex = node.id.properties.findIndex((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'container');
const nodeValue = containerIndex !== -1 && node.id.properties[containerIndex].value;
if (!nodeValue) {
return;
}
if (utils_1.ASTUtils.isIdentifier(nodeValue)) {
containerName = nodeValue.name;
}
else if ((0, node_utils_1.isObjectPattern)(nodeValue)) {
nodeValue.properties.forEach((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
destructuredContainerPropNames.push(property.key.name));
}
}
else if (utils_1.ASTUtils.isIdentifier(node.id)) {
renderResultVarName = node.id.name;
}
},
};
},
});

View File

@@ -0,0 +1,125 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
exports.RULE_NAME = 'no-debugging-utils';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of debugging utilities like `debug`',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noDebug: 'Unexpected debug statement',
},
schema: [
{
type: 'object',
properties: {
utilsToCheckFor: {
type: 'object',
properties: utils_2.DEBUG_UTILS.reduce((obj, name) => (Object.assign({ [name]: { type: 'boolean' } }, obj)), {}),
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [
{ utilsToCheckFor: { debug: true, logTestingPlaygroundURL: true } },
],
create(context, [{ utilsToCheckFor = {} }], helpers) {
const suspiciousDebugVariableNames = [];
const suspiciousReferenceNodes = [];
const renderWrapperNames = [];
const builtInConsoleNodes = [];
const utilsToReport = Object.entries(utilsToCheckFor)
.filter(([, shouldCheckFor]) => shouldCheckFor)
.map(([name]) => name);
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
VariableDeclarator(node) {
if (!node.init) {
return;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return;
}
if (initIdentifierNode.name === 'console') {
builtInConsoleNodes.push(node);
return;
}
const isRenderWrapperVariableDeclarator = renderWrapperNames.includes(initIdentifierNode.name);
if (!helpers.isRenderVariableDeclarator(node) &&
!isRenderWrapperVariableDeclarator) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
for (const property of node.id.properties) {
if ((0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
utilsToReport.includes(property.key.name)) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(property.value);
if (identifierNode) {
suspiciousDebugVariableNames.push(identifierNode.name);
}
}
}
}
if (utils_1.ASTUtils.isIdentifier(node.id)) {
suspiciousReferenceNodes.push(node.id);
}
},
CallExpression(node) {
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
if (helpers.isRenderUtil(callExpressionIdentifier)) {
detectRenderWrapper(callExpressionIdentifier);
}
const referenceNode = (0, node_utils_1.getReferenceNode)(node);
const referenceIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(referenceNode);
if (!referenceIdentifier) {
return;
}
const isDebugUtil = helpers.isDebugUtil(callExpressionIdentifier, utilsToReport);
const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes(callExpressionIdentifier.name);
const isChainedReferenceDebug = suspiciousReferenceNodes.some((suspiciousReferenceIdentifier) => {
return (utilsToReport.includes(callExpressionIdentifier.name) &&
suspiciousReferenceIdentifier.name === referenceIdentifier.name);
});
const isVariableFromBuiltInConsole = builtInConsoleNodes.some((variableDeclarator) => {
const variables = context.getDeclaredVariables(variableDeclarator);
return variables.some(({ name }) => name === callExpressionIdentifier.name &&
(0, node_utils_1.isCallExpression)(callExpressionIdentifier.parent));
});
if (!isVariableFromBuiltInConsole &&
(isDebugUtil || isDeclaredDebugVariable || isChainedReferenceDebug)) {
context.report({
node: callExpressionIdentifier,
messageId: 'noDebug',
});
}
},
};
},
});

View File

@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-dom-import';
const DOM_TESTING_LIBRARY_MODULES = [
'dom-testing-library',
'@testing-library/dom',
];
const CORRECT_MODULE_NAME_BY_FRAMEWORK = {
angular: '@testing-library/angular',
marko: '@marko/testing-library',
};
const getCorrectModuleName = (moduleName, framework) => {
var _a;
return ((_a = CORRECT_MODULE_NAME_BY_FRAMEWORK[framework]) !== null && _a !== void 0 ? _a : moduleName.replace('dom', framework));
};
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow importing from DOM Testing Library',
recommendedConfig: {
dom: false,
angular: ['error', 'angular'],
react: ['error', 'react'],
vue: ['error', 'vue'],
marko: ['error', 'marko'],
},
},
messages: {
noDomImport: 'import from DOM Testing Library is restricted, import from corresponding Testing Library framework instead',
noDomImportFramework: 'import from DOM Testing Library is restricted, import from {{module}} instead',
},
fixable: 'code',
schema: [{ type: 'string' }],
},
defaultOptions: [''],
create(context, [framework], helpers) {
function report(node, moduleName) {
if (!framework) {
return context.report({
node,
messageId: 'noDomImport',
});
}
const correctModuleName = getCorrectModuleName(moduleName, framework);
context.report({
data: { module: correctModuleName },
fix(fixer) {
if ((0, node_utils_1.isCallExpression)(node)) {
const name = node.arguments[0];
return fixer.replaceText(name, name.raw.replace(moduleName, correctModuleName));
}
else {
const name = node.source;
return fixer.replaceText(name, name.raw.replace(moduleName, correctModuleName));
}
},
messageId: 'noDomImportFramework',
node,
});
}
return {
'Program:exit'() {
let importName;
const allImportNodes = helpers.getAllTestingLibraryImportNodes();
allImportNodes.forEach((importNode) => {
importName = (0, node_utils_1.getImportModuleName)(importNode);
const domModuleName = DOM_TESTING_LIBRARY_MODULES.find((module) => module === importName);
if (!domModuleName) {
return;
}
report(importNode, domModuleName);
});
},
};
},
});

View File

@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-global-regexp-flag-in-query';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of the global RegExp flag (/g) in queries',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
noGlobalRegExpFlagInQuery: 'Avoid using the global RegExp flag (/g) in queries',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function reportLiteralWithRegex(literalNode) {
if ((0, node_utils_1.isLiteral)(literalNode) &&
'regex' in literalNode &&
literalNode.regex.flags.includes('g')) {
context.report({
node: literalNode,
messageId: 'noGlobalRegExpFlagInQuery',
fix(fixer) {
const splitter = literalNode.raw.lastIndexOf('/');
const raw = literalNode.raw.substring(0, splitter);
const flags = literalNode.raw.substring(splitter + 1);
const flagsWithoutGlobal = flags.replace('g', '');
return fixer.replaceText(literalNode, `${raw}/${flagsWithoutGlobal}`);
},
});
return true;
}
return false;
}
function getArguments(identifierNode) {
if ((0, node_utils_1.isCallExpression)(identifierNode.parent)) {
return identifierNode.parent.arguments;
}
else if ((0, node_utils_1.isMemberExpression)(identifierNode.parent) &&
(0, node_utils_1.isCallExpression)(identifierNode.parent.parent)) {
return identifierNode.parent.parent.arguments;
}
return [];
}
const variableNodesWithRegexs = [];
function hasRegexInVariable(identifier) {
return variableNodesWithRegexs.find((varNode) => {
if (utils_1.ASTUtils.isVariableDeclarator(varNode) &&
utils_1.ASTUtils.isIdentifier(varNode.id)) {
return varNode.id.name === identifier.name;
}
return undefined;
});
}
return {
VariableDeclarator(node) {
if (utils_1.ASTUtils.isVariableDeclarator(node) &&
(0, node_utils_1.isLiteral)(node.init) &&
'regex' in node.init &&
node.init.regex.flags.includes('g')) {
variableNodesWithRegexs.push(node);
}
},
CallExpression(node) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!identifierNode || !helpers.isQuery(identifierNode)) {
return;
}
const [firstArg, secondArg] = getArguments(identifierNode);
const firstArgumentHasError = reportLiteralWithRegex(firstArg);
if (firstArgumentHasError) {
return;
}
if (utils_1.ASTUtils.isIdentifier(firstArg)) {
const regexVariableNode = hasRegexInVariable(firstArg);
if (regexVariableNode !== undefined) {
context.report({
node: firstArg,
messageId: 'noGlobalRegExpFlagInQuery',
fix(fixer) {
if (utils_1.ASTUtils.isVariableDeclarator(regexVariableNode) &&
(0, node_utils_1.isLiteral)(regexVariableNode.init) &&
'regex' in regexVariableNode.init &&
regexVariableNode.init.regex.flags.includes('g')) {
const splitter = regexVariableNode.init.raw.lastIndexOf('/');
const raw = regexVariableNode.init.raw.substring(0, splitter);
const flags = regexVariableNode.init.raw.substring(splitter + 1);
const flagsWithoutGlobal = flags.replace('g', '');
return fixer.replaceText(regexVariableNode.init, `${raw}/${flagsWithoutGlobal}`);
}
return null;
},
});
}
}
if ((0, node_utils_1.isObjectExpression)(secondArg)) {
const namePropertyNode = secondArg.properties.find((p) => (0, node_utils_1.isProperty)(p) &&
utils_1.ASTUtils.isIdentifier(p.key) &&
p.key.name === 'name' &&
(0, node_utils_1.isLiteral)(p.value));
if (namePropertyNode) {
reportLiteralWithRegex(namePropertyNode.value);
}
}
},
};
},
});

View File

@@ -0,0 +1,94 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-manual-cleanup';
const CLEANUP_LIBRARY_REGEXP = /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/;
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `cleanup`',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
noManualCleanup: "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.",
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function reportImportReferences(references) {
for (const reference of references) {
const utilsUsage = reference.identifier.parent;
if (utilsUsage &&
(0, node_utils_1.isMemberExpression)(utilsUsage) &&
utils_1.ASTUtils.isIdentifier(utilsUsage.property) &&
utilsUsage.property.name === 'cleanup') {
context.report({
node: utilsUsage.property,
messageId: 'noManualCleanup',
});
}
}
}
function reportCandidateModule(moduleNode) {
if ((0, node_utils_1.isImportDeclaration)(moduleNode)) {
if ((0, node_utils_1.isImportDefaultSpecifier)(moduleNode.specifiers[0])) {
const { references } = context.getDeclaredVariables(moduleNode)[0];
reportImportReferences(references);
}
const cleanupSpecifier = moduleNode.specifiers.find((specifier) => (0, node_utils_1.isImportSpecifier)(specifier) &&
specifier.imported.name === 'cleanup');
if (cleanupSpecifier) {
context.report({
node: cleanupSpecifier,
messageId: 'noManualCleanup',
});
}
}
else {
const declaratorNode = moduleNode.parent;
if ((0, node_utils_1.isObjectPattern)(declaratorNode.id)) {
const cleanupProperty = declaratorNode.id.properties.find((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
property.key.name === 'cleanup');
if (cleanupProperty) {
context.report({
node: cleanupProperty,
messageId: 'noManualCleanup',
});
}
}
else {
const references = (0, node_utils_1.getVariableReferences)(context, declaratorNode);
reportImportReferences(references);
}
}
}
return {
'Program:exit'() {
const testingLibraryImportName = helpers.getTestingLibraryImportName();
const testingLibraryImportNode = helpers.getTestingLibraryImportNode();
const customModuleImportNode = helpers.getCustomModuleImportNode();
if (testingLibraryImportName &&
testingLibraryImportNode &&
testingLibraryImportName.match(CLEANUP_LIBRARY_REGEXP)) {
reportCandidateModule(testingLibraryImportNode);
}
if (customModuleImportNode) {
reportCandidateModule(customModuleImportNode);
}
},
};
},
});

View File

@@ -0,0 +1,67 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const utils_2 = require("../utils");
exports.RULE_NAME = 'no-node-access';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow direct Node access',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noNodeAccess: 'Avoid direct Node access. Prefer using the methods from Testing Library.',
},
schema: [
{
type: 'object',
properties: {
allowContainerFirstChild: {
type: 'boolean',
},
},
},
],
},
defaultOptions: [
{
allowContainerFirstChild: false,
},
],
create(context, [{ allowContainerFirstChild = false }], helpers) {
function showErrorForNodeAccess(node) {
if (!helpers.isTestingLibraryImported(true)) {
return;
}
if (utils_1.ASTUtils.isIdentifier(node.property) &&
utils_2.ALL_RETURNING_NODES.includes(node.property.name)) {
if (allowContainerFirstChild && node.property.name === 'firstChild') {
return;
}
if (utils_1.ASTUtils.isIdentifier(node.object) &&
node.object.name === 'props') {
return;
}
context.report({
node,
loc: node.property.loc.start,
messageId: 'noNodeAccess',
});
}
}
return {
'ExpressionStatement MemberExpression': showErrorForNodeAccess,
'VariableDeclarator MemberExpression': showErrorForNodeAccess,
};
},
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-promise-in-fire-event';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of promises passed to a `fireEvent` method',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noPromiseInFireEvent: "A promise shouldn't be passed to a `fireEvent` method, instead pass the DOM element",
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function checkSuspiciousNode(node, originalNode) {
if (utils_1.ASTUtils.isAwaitExpression(node)) {
return;
}
if ((0, node_utils_1.isNewExpression)(node)) {
if ((0, node_utils_1.isPromiseIdentifier)(node.callee)) {
context.report({
node: originalNode !== null && originalNode !== void 0 ? originalNode : node,
messageId: 'noPromiseInFireEvent',
});
return;
}
}
if ((0, node_utils_1.isCallExpression)(node)) {
const domElementIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!domElementIdentifier) {
return;
}
if (helpers.isAsyncQuery(domElementIdentifier) ||
(0, node_utils_1.isPromiseIdentifier)(domElementIdentifier)) {
context.report({
node: originalNode !== null && originalNode !== void 0 ? originalNode : node,
messageId: 'noPromiseInFireEvent',
});
return;
}
}
if (utils_1.ASTUtils.isIdentifier(node)) {
const nodeVariable = utils_1.ASTUtils.findVariable(context.getScope(), node.name);
if (!nodeVariable) {
return;
}
for (const definition of nodeVariable.defs) {
const variableDeclarator = definition.node;
if (variableDeclarator.init) {
checkSuspiciousNode(variableDeclarator.init, node);
}
}
}
}
return {
'CallExpression Identifier'(node) {
if (!helpers.isFireEventMethod(node)) {
return;
}
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!closestCallExpression) {
return;
}
const domElementArgument = closestCallExpression.arguments[0];
checkSuspiciousNode(domElementArgument);
},
};
},
});

View File

@@ -0,0 +1,94 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findClosestBeforeHook = exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
exports.RULE_NAME = 'no-render-in-setup';
function findClosestBeforeHook(node, testingFrameworkSetupHooksToFilter) {
if (node === null) {
return null;
}
if ((0, node_utils_1.isCallExpression)(node) &&
utils_1.ASTUtils.isIdentifier(node.callee) &&
testingFrameworkSetupHooksToFilter.includes(node.callee.name)) {
return node.callee;
}
if (node.parent) {
return findClosestBeforeHook(node.parent, testingFrameworkSetupHooksToFilter);
}
return null;
}
exports.findClosestBeforeHook = findClosestBeforeHook;
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow the use of `render` in testing frameworks setup functions',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noRenderInSetup: 'Forbidden usage of `render` within testing framework `{{ name }}` setup',
},
schema: [
{
type: 'object',
properties: {
allowTestingFrameworkSetupHook: {
enum: utils_2.TESTING_FRAMEWORK_SETUP_HOOKS,
},
},
},
],
},
defaultOptions: [
{
allowTestingFrameworkSetupHook: '',
},
],
create(context, [{ allowTestingFrameworkSetupHook }], helpers) {
const renderWrapperNames = [];
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
CallExpression(node) {
const testingFrameworkSetupHooksToFilter = utils_2.TESTING_FRAMEWORK_SETUP_HOOKS.filter((hook) => hook !== allowTestingFrameworkSetupHook);
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
const isRenderIdentifier = helpers.isRenderUtil(callExpressionIdentifier);
if (isRenderIdentifier) {
detectRenderWrapper(callExpressionIdentifier);
}
if (!isRenderIdentifier &&
!renderWrapperNames.includes(callExpressionIdentifier.name)) {
return;
}
const beforeHook = findClosestBeforeHook(node, testingFrameworkSetupHooksToFilter);
if (!beforeHook) {
return;
}
context.report({
node: callExpressionIdentifier,
messageId: 'noRenderInSetup',
data: {
name: beforeHook.name,
},
});
},
};
},
});

View File

@@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-unnecessary-act';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow wrapping Testing Library utils or empty callbacks in `act`',
recommendedConfig: {
dom: false,
angular: false,
react: 'error',
vue: false,
marko: 'error',
},
},
messages: {
noUnnecessaryActTestingLibraryUtil: 'Avoid wrapping Testing Library util calls in `act`',
noUnnecessaryActEmptyFunction: 'Avoid wrapping empty function in `act`',
},
schema: [
{
type: 'object',
properties: {
isStrict: {
type: 'boolean',
},
},
},
],
},
defaultOptions: [
{
isStrict: true,
},
],
create(context, [{ isStrict = true }], helpers) {
function getStatementIdentifier(statement) {
const callExpression = (0, node_utils_1.getStatementCallExpression)(statement);
if (!callExpression &&
!(0, node_utils_1.isExpressionStatement)(statement) &&
!(0, node_utils_1.isReturnStatement)(statement)) {
return null;
}
if (callExpression) {
return (0, node_utils_1.getDeepestIdentifierNode)(callExpression);
}
if ((0, node_utils_1.isExpressionStatement)(statement) &&
utils_1.ASTUtils.isAwaitExpression(statement.expression)) {
return (0, node_utils_1.getPropertyIdentifierNode)(statement.expression.argument);
}
if ((0, node_utils_1.isReturnStatement)(statement) && statement.argument) {
return (0, node_utils_1.getPropertyIdentifierNode)(statement.argument);
}
return null;
}
function hasSomeNonTestingLibraryCall(statements) {
return statements.some((statement) => {
const identifier = getStatementIdentifier(statement);
if (!identifier) {
return false;
}
return !helpers.isTestingLibraryUtil(identifier);
});
}
function hasTestingLibraryCall(statements) {
return statements.some((statement) => {
const identifier = getStatementIdentifier(statement);
if (!identifier) {
return false;
}
return helpers.isTestingLibraryUtil(identifier);
});
}
function checkNoUnnecessaryActFromBlockStatement(blockStatementNode) {
const functionNode = blockStatementNode.parent;
const callExpressionNode = functionNode === null || functionNode === void 0 ? void 0 : functionNode.parent;
if (!callExpressionNode || !functionNode) {
return;
}
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(callExpressionNode);
if (!identifierNode) {
return;
}
if (!helpers.isActUtil(identifierNode)) {
return;
}
if ((0, node_utils_1.isEmptyFunction)(functionNode)) {
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActEmptyFunction',
});
return;
}
const shouldBeReported = isStrict
? hasTestingLibraryCall(blockStatementNode.body)
: !hasSomeNonTestingLibraryCall(blockStatementNode.body);
if (shouldBeReported) {
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActTestingLibraryUtil',
});
}
}
function checkNoUnnecessaryActFromImplicitReturn(node) {
var _a;
const nodeIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!nodeIdentifier) {
return;
}
const parentCallExpression = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent;
if (!parentCallExpression) {
return;
}
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(parentCallExpression);
if (!identifierNode) {
return;
}
if (!helpers.isActUtil(identifierNode)) {
return;
}
if (!helpers.isTestingLibraryUtil(nodeIdentifier)) {
return;
}
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActTestingLibraryUtil',
});
}
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': checkNoUnnecessaryActFromBlockStatement,
'CallExpression > FunctionExpression > BlockStatement': checkNoUnnecessaryActFromBlockStatement,
'CallExpression > ArrowFunctionExpression > CallExpression': checkNoUnnecessaryActFromImplicitReturn,
};
},
});

View File

@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-empty-callback';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved`',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noWaitForEmptyCallback: 'Avoid passing empty callback to `{{ methodName }}`. Insert an assertion instead.',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function isValidWaitFor(node) {
const parentCallExpression = node.parent;
const parentIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(parentCallExpression);
if (!parentIdentifier) {
return false;
}
return helpers.isAsyncUtil(parentIdentifier, [
'waitFor',
'waitForElementToBeRemoved',
]);
}
function reportIfEmpty(node) {
if (!isValidWaitFor(node)) {
return;
}
if ((0, node_utils_1.isEmptyFunction)(node) &&
(0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isIdentifier(node.parent.callee)) {
context.report({
node,
loc: node.body.loc.start,
messageId: 'noWaitForEmptyCallback',
data: {
methodName: node.parent.callee.name,
},
});
}
}
function reportNoop(node) {
if (!isValidWaitFor(node)) {
return;
}
context.report({
node,
loc: node.loc.start,
messageId: 'noWaitForEmptyCallback',
data: {
methodName: (0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isIdentifier(node.parent.callee) &&
node.parent.callee.name,
},
});
}
return {
'CallExpression > ArrowFunctionExpression': reportIfEmpty,
'CallExpression > FunctionExpression': reportIfEmpty,
'CallExpression > Identifier[name="noop"]': reportNoop,
};
},
});

View File

@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-multiple-assertions';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of multiple `expect` calls inside `waitFor`',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noWaitForMultipleAssertion: 'Avoid using multiple assertions within `waitFor` callback',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function getExpectNodes(body) {
return body.filter((node) => {
if (!(0, node_utils_1.isExpressionStatement)(node)) {
return false;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!expressionIdentifier) {
return false;
}
return expressionIdentifier.name === 'expect';
});
}
function reportMultipleAssertion(node) {
if (!node.parent) {
return;
}
const callExpressionNode = node.parent.parent;
const callExpressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(callExpressionNode);
if (!callExpressionIdentifier) {
return;
}
if (!helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor'])) {
return;
}
const expectNodes = getExpectNodes(node.body);
if (expectNodes.length <= 1) {
return;
}
for (let i = 0; i < expectNodes.length; i++) {
if (i !== 0) {
context.report({
node: expectNodes[i],
messageId: 'noWaitForMultipleAssertion',
});
}
}
}
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': reportMultipleAssertion,
'CallExpression > FunctionExpression > BlockStatement': reportMultipleAssertion,
};
},
});

View File

@@ -0,0 +1,155 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-side-effects';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of side effects in `waitFor`',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function isCallerWaitFor(node) {
if (!node.parent) {
return false;
}
const callExpressionNode = node.parent.parent;
const callExpressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(callExpressionNode);
return (!!callExpressionIdentifier &&
helpers.isAsyncUtil(callExpressionIdentifier, ['waitFor']));
}
function isCallerThen(node) {
if (!node.parent) {
return false;
}
const callExpressionNode = node.parent.parent;
return (0, node_utils_1.hasThenProperty)(callExpressionNode.callee);
}
function isRenderInVariableDeclaration(node) {
return ((0, node_utils_1.isVariableDeclaration)(node) &&
node.declarations.some(helpers.isRenderVariableDeclarator));
}
function isRenderInExpressionStatement(node) {
if (!(0, node_utils_1.isExpressionStatement)(node) ||
!(0, node_utils_1.isAssignmentExpression)(node.expression)) {
return false;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node.expression.right);
if (!expressionIdentifier) {
return false;
}
return helpers.isRenderUtil(expressionIdentifier);
}
function isRenderInAssignmentExpression(node) {
if (!(0, node_utils_1.isAssignmentExpression)(node)) {
return false;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node.right);
if (!expressionIdentifier) {
return false;
}
return helpers.isRenderUtil(expressionIdentifier);
}
function isRenderInSequenceAssignment(node) {
if (!(0, node_utils_1.isSequenceExpression)(node)) {
return false;
}
return node.expressions.some(isRenderInAssignmentExpression);
}
function isSideEffectInVariableDeclaration(node) {
return node.declarations.some((declaration) => {
if ((0, node_utils_1.isCallExpression)(declaration.init)) {
const test = (0, node_utils_1.getPropertyIdentifierNode)(declaration.init);
if (!test) {
return false;
}
return (helpers.isFireEventUtil(test) ||
helpers.isUserEventUtil(test) ||
helpers.isRenderUtil(test));
}
return false;
});
return false;
}
function getSideEffectNodes(body) {
return body.filter((node) => {
if (!(0, node_utils_1.isExpressionStatement)(node) && !(0, node_utils_1.isVariableDeclaration)(node)) {
return false;
}
if (isRenderInVariableDeclaration(node) ||
isRenderInExpressionStatement(node)) {
return true;
}
if ((0, node_utils_1.isVariableDeclaration)(node) &&
isSideEffectInVariableDeclaration(node)) {
return true;
}
const expressionIdentifier = (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!expressionIdentifier) {
return false;
}
return (helpers.isFireEventUtil(expressionIdentifier) ||
helpers.isUserEventUtil(expressionIdentifier) ||
helpers.isRenderUtil(expressionIdentifier));
});
}
function reportSideEffects(node) {
if (!isCallerWaitFor(node)) {
return;
}
if (isCallerThen(node)) {
return;
}
getSideEffectNodes(node.body).forEach((sideEffectNode) => context.report({
node: sideEffectNode,
messageId: 'noSideEffectsWaitFor',
}));
}
function reportImplicitReturnSideEffect(node) {
if (!isCallerWaitFor(node)) {
return;
}
const expressionIdentifier = (0, node_utils_1.isCallExpression)(node)
? (0, node_utils_1.getPropertyIdentifierNode)(node.callee)
: null;
if (!expressionIdentifier &&
!isRenderInAssignmentExpression(node) &&
!isRenderInSequenceAssignment(node)) {
return;
}
if (expressionIdentifier &&
!helpers.isFireEventUtil(expressionIdentifier) &&
!helpers.isUserEventUtil(expressionIdentifier) &&
!helpers.isRenderUtil(expressionIdentifier)) {
return;
}
context.report({
node,
messageId: 'noSideEffectsWaitFor',
});
}
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': reportSideEffects,
'CallExpression > ArrowFunctionExpression > CallExpression': reportImplicitReturnSideEffect,
'CallExpression > ArrowFunctionExpression > AssignmentExpression': reportImplicitReturnSideEffect,
'CallExpression > ArrowFunctionExpression > SequenceExpression': reportImplicitReturnSideEffect,
'CallExpression > FunctionExpression > BlockStatement': reportSideEffects,
};
},
});

View File

@@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'no-wait-for-snapshot';
const SNAPSHOT_REGEXP = /^(toMatchSnapshot|toMatchInlineSnapshot)$/;
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Ensures no snapshot is generated inside of a `waitFor` call',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
noWaitForSnapshot: "A snapshot can't be generated inside of a `{{ name }}` call",
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function getClosestAsyncUtil(node) {
let n = node;
do {
const callExpression = (0, node_utils_1.findClosestCallExpressionNode)(n);
if (!callExpression) {
return null;
}
if (utils_1.ASTUtils.isIdentifier(callExpression.callee) &&
helpers.isAsyncUtil(callExpression.callee)) {
return callExpression.callee;
}
if ((0, node_utils_1.isMemberExpression)(callExpression.callee) &&
utils_1.ASTUtils.isIdentifier(callExpression.callee.property) &&
helpers.isAsyncUtil(callExpression.callee.property)) {
return callExpression.callee.property;
}
if (callExpression.parent) {
n = (0, node_utils_1.findClosestCallExpressionNode)(callExpression.parent);
}
} while (n !== null);
return null;
}
return {
[`Identifier[name=${String(SNAPSHOT_REGEXP)}]`](node) {
const closestAsyncUtil = getClosestAsyncUtil(node);
if (closestAsyncUtil === null) {
return;
}
context.report({
node,
messageId: 'noWaitForSnapshot',
data: { name: closestAsyncUtil.name },
});
},
};
},
});

View File

@@ -0,0 +1,154 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
const utils_2 = require("../utils");
exports.RULE_NAME = 'prefer-explicit-assert';
const isAtTopLevel = (node) => {
var _a, _b, _c;
return (!!((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) &&
node.parent.parent.type === 'ExpressionStatement') ||
(((_c = (_b = node.parent) === null || _b === void 0 ? void 0 : _b.parent) === null || _c === void 0 ? void 0 : _c.type) === 'AwaitExpression' &&
!!node.parent.parent.parent &&
node.parent.parent.parent.type === 'ExpressionStatement');
};
const isVariableDeclaration = (node) => {
if ((0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isAwaitExpression(node.parent.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent.parent)) {
return true;
}
if ((0, node_utils_1.isCallExpression)(node.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent)) {
return true;
}
if ((0, node_utils_1.isMemberExpression)(node.parent) &&
(0, node_utils_1.isCallExpression)(node.parent.parent) &&
utils_1.ASTUtils.isAwaitExpression(node.parent.parent.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent.parent.parent)) {
return true;
}
if ((0, node_utils_1.isMemberExpression)(node.parent) &&
(0, node_utils_1.isCallExpression)(node.parent.parent) &&
utils_1.ASTUtils.isVariableDeclarator(node.parent.parent.parent)) {
return true;
}
return false;
};
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using explicit assertions rather than standalone queries',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
preferExplicitAssert: 'Wrap stand-alone `{{queryType}}` query with `expect` function for better explicit assertion',
preferExplicitAssertAssertion: '`getBy*` queries must be asserted with `{{assertion}}`',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
assertion: {
type: 'string',
enum: utils_2.PRESENCE_MATCHERS,
},
includeFindQueries: { type: 'boolean' },
},
},
],
},
defaultOptions: [{ includeFindQueries: true }],
create(context, [options], helpers) {
const { assertion, includeFindQueries } = options;
const getQueryCalls = [];
const findQueryCalls = [];
return {
'CallExpression Identifier'(node) {
if (helpers.isGetQueryVariant(node)) {
getQueryCalls.push(node);
}
if (helpers.isFindQueryVariant(node)) {
findQueryCalls.push(node);
}
},
'Program:exit'() {
if (includeFindQueries) {
findQueryCalls.forEach((queryCall) => {
const memberExpression = (0, node_utils_1.isMemberExpression)(queryCall.parent)
? queryCall.parent
: queryCall;
if (isVariableDeclaration(queryCall) ||
!isAtTopLevel(memberExpression)) {
return;
}
context.report({
node: queryCall,
messageId: 'preferExplicitAssert',
data: {
queryType: 'findBy*',
},
});
});
}
getQueryCalls.forEach((queryCall) => {
const node = (0, node_utils_1.isMemberExpression)(queryCall.parent)
? queryCall.parent
: queryCall;
if (isAtTopLevel(node)) {
context.report({
node: queryCall,
messageId: 'preferExplicitAssert',
data: {
queryType: 'getBy*',
},
});
}
if (assertion) {
const expectCallNode = (0, node_utils_1.findClosestCallNode)(node, 'expect');
if (!expectCallNode)
return;
const expectStatement = expectCallNode.parent;
if (!(0, node_utils_1.isMemberExpression)(expectStatement)) {
return;
}
const property = expectStatement.property;
if (!utils_1.ASTUtils.isIdentifier(property)) {
return;
}
let matcher = property.name;
let isNegatedMatcher = false;
if (matcher === 'not' &&
(0, node_utils_1.isMemberExpression)(expectStatement.parent) &&
utils_1.ASTUtils.isIdentifier(expectStatement.parent.property)) {
isNegatedMatcher = true;
matcher = expectStatement.parent.property.name;
}
const shouldEnforceAssertion = (!isNegatedMatcher && utils_2.PRESENCE_MATCHERS.includes(matcher)) ||
(isNegatedMatcher && utils_2.ABSENCE_MATCHERS.includes(matcher));
if (shouldEnforceAssertion && matcher !== assertion) {
context.report({
node: property,
messageId: 'preferExplicitAssertAssertion',
data: {
assertion,
},
});
}
}
});
},
};
},
});

View File

@@ -0,0 +1,293 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFindByQueryVariant = exports.WAIT_METHODS = exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-find-by';
exports.WAIT_METHODS = ['waitFor', 'waitForElement', 'wait'];
function getFindByQueryVariant(queryMethod) {
return queryMethod.includes('All') ? 'findAllBy' : 'findBy';
}
exports.getFindByQueryVariant = getFindByQueryVariant;
function findRenderDefinitionDeclaration(scope, query) {
var _a;
if (!scope) {
return null;
}
const variable = scope.variables.find((v) => v.name === query);
if (variable) {
return ((_a = variable.defs
.map(({ name }) => name)
.filter(utils_1.ASTUtils.isIdentifier)
.find(({ name }) => name === query)) !== null && _a !== void 0 ? _a : null);
}
return findRenderDefinitionDeclaration(scope.upper, query);
}
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
preferFindBy: 'Prefer `{{queryVariant}}{{queryMethod}}` query over using `{{waitForMethodName}}` + `{{prevQuery}}`',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const sourceCode = context.getSourceCode();
function reportInvalidUsage(node, replacementParams) {
const { queryMethod, queryVariant, prevQuery, waitForMethodName, fix } = replacementParams;
context.report({
node,
messageId: 'preferFindBy',
data: {
queryVariant,
queryMethod,
prevQuery,
waitForMethodName,
},
fix,
});
}
function getWrongQueryNameInAssertion(node) {
if (!(0, node_utils_1.isCallExpression)(node.body) ||
!(0, node_utils_1.isMemberExpression)(node.body.callee)) {
return null;
}
if ((0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee)) {
return node.body.callee.object.arguments[0].callee.name;
}
if (!utils_1.ASTUtils.isIdentifier(node.body.callee.property)) {
return null;
}
if ((0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee.property)) {
return node.body.callee.object.arguments[0].callee.property.name;
}
if ((0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee.property)) {
return node.body.callee.object.object.arguments[0].callee.property.name;
}
if ((0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee)) {
return node.body.callee.object.object.arguments[0].callee.name;
}
return node.body.callee.property.name;
}
function getWrongQueryName(node) {
if (!(0, node_utils_1.isCallExpression)(node.body)) {
return null;
}
if (utils_1.ASTUtils.isIdentifier(node.body.callee) &&
helpers.isSyncQuery(node.body.callee)) {
return node.body.callee.name;
}
return getWrongQueryNameInAssertion(node);
}
function getCaller(node) {
if (!(0, node_utils_1.isCallExpression)(node.body) ||
!(0, node_utils_1.isMemberExpression)(node.body.callee)) {
return null;
}
if (utils_1.ASTUtils.isIdentifier(node.body.callee.object)) {
return node.body.callee.object.name;
}
if ((0, node_utils_1.isCallExpression)(node.body.callee.object) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.callee) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee.object)) {
return node.body.callee.object.arguments[0].callee.object.name;
}
if ((0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee.object)) {
return node.body.callee.object.object.arguments[0].callee.object.name;
}
return null;
}
function isSyncQuery(node) {
if (!(0, node_utils_1.isCallExpression)(node.body)) {
return false;
}
const isQuery = utils_1.ASTUtils.isIdentifier(node.body.callee) &&
helpers.isSyncQuery(node.body.callee);
const isWrappedInPresenceAssert = (0, node_utils_1.isMemberExpression)(node.body.callee) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) &&
helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) &&
helpers.isPresenceAssert(node.body.callee);
const isWrappedInNegatedPresenceAssert = (0, node_utils_1.isMemberExpression)(node.body.callee) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.object.arguments[0].callee) &&
helpers.isSyncQuery(node.body.callee.object.object.arguments[0].callee) &&
helpers.isPresenceAssert(node.body.callee.object);
return (isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert);
}
function isScreenSyncQuery(node) {
if (!(0, node_utils_1.isArrowFunctionExpression)(node) || !(0, node_utils_1.isCallExpression)(node.body)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.body.callee) ||
!utils_1.ASTUtils.isIdentifier(node.body.callee.property)) {
return false;
}
if (!utils_1.ASTUtils.isIdentifier(node.body.callee.object) &&
!(0, node_utils_1.isCallExpression)(node.body.callee.object) &&
!(0, node_utils_1.isMemberExpression)(node.body.callee.object)) {
return false;
}
const isWrappedInPresenceAssert = helpers.isPresenceAssert(node.body.callee) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.arguments[0].callee) &&
utils_1.ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee.object);
const isWrappedInNegatedPresenceAssert = (0, node_utils_1.isMemberExpression)(node.body.callee.object) &&
helpers.isPresenceAssert(node.body.callee.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.body.callee.object.object.arguments[0]) &&
(0, node_utils_1.isMemberExpression)(node.body.callee.object.object.arguments[0].callee);
return (helpers.isSyncQuery(node.body.callee.property) ||
isWrappedInPresenceAssert ||
isWrappedInNegatedPresenceAssert);
}
function getQueryArguments(node) {
if ((0, node_utils_1.isMemberExpression)(node.callee) &&
(0, node_utils_1.isCallExpression)(node.callee.object) &&
(0, node_utils_1.isCallExpression)(node.callee.object.arguments[0])) {
return node.callee.object.arguments[0].arguments;
}
if ((0, node_utils_1.isMemberExpression)(node.callee) &&
(0, node_utils_1.isMemberExpression)(node.callee.object) &&
(0, node_utils_1.isCallExpression)(node.callee.object.object) &&
(0, node_utils_1.isCallExpression)(node.callee.object.object.arguments[0])) {
return node.callee.object.object.arguments[0].arguments;
}
return node.arguments;
}
return {
'AwaitExpression > CallExpression'(node) {
if (!utils_1.ASTUtils.isIdentifier(node.callee) ||
!helpers.isAsyncUtil(node.callee, exports.WAIT_METHODS)) {
return;
}
const argument = node.arguments[0];
if (!(0, node_utils_1.isArrowFunctionExpression)(argument) ||
!(0, node_utils_1.isCallExpression)(argument.body)) {
return;
}
const waitForMethodName = node.callee.name;
if (isScreenSyncQuery(argument)) {
const caller = getCaller(argument);
if (!caller) {
return;
}
const fullQueryMethod = getWrongQueryName(argument);
if (!fullQueryMethod) {
return;
}
const waitOptions = node.arguments[1];
let waitOptionsSourceCode = '';
if ((0, node_utils_1.isObjectExpression)(waitOptions)) {
waitOptionsSourceCode = `, ${sourceCode.getText(waitOptions)}`;
}
const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = getQueryArguments(argument.body);
const queryMethod = fullQueryMethod.split('By')[1];
if (!queryMethod) {
return;
}
reportInvalidUsage(node, {
queryMethod,
queryVariant,
prevQuery: fullQueryMethod,
waitForMethodName,
fix(fixer) {
const property = argument.body
.callee.property;
if (helpers.isCustomQuery(property)) {
return null;
}
const newCode = `${caller}.${queryVariant}${queryMethod}(${callArguments
.map((callArgNode) => sourceCode.getText(callArgNode))
.join(', ')}${waitOptionsSourceCode})`;
return fixer.replaceText(node, newCode);
},
});
return;
}
if (!isSyncQuery(argument)) {
return;
}
const fullQueryMethod = getWrongQueryName(argument);
if (!fullQueryMethod) {
return;
}
const queryMethod = fullQueryMethod.split('By')[1];
const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = getQueryArguments(argument.body);
reportInvalidUsage(node, {
queryMethod,
queryVariant,
prevQuery: fullQueryMethod,
waitForMethodName,
fix(fixer) {
if (helpers.isCustomQuery(argument.body
.callee)) {
return null;
}
const findByMethod = `${queryVariant}${queryMethod}`;
const allFixes = [];
const newCode = `${findByMethod}(${callArguments
.map((callArgNode) => sourceCode.getText(callArgNode))
.join(', ')})`;
allFixes.push(fixer.replaceText(node, newCode));
const definition = findRenderDefinitionDeclaration(context.getScope(), fullQueryMethod);
if (!definition) {
return allFixes;
}
if (definition.parent &&
(0, node_utils_1.isObjectPattern)(definition.parent.parent)) {
const allVariableDeclarations = definition.parent.parent;
if (allVariableDeclarations.properties.some((p) => (0, node_utils_1.isProperty)(p) &&
utils_1.ASTUtils.isIdentifier(p.key) &&
p.key.name === findByMethod)) {
return allFixes;
}
const textDestructuring = sourceCode.getText(allVariableDeclarations);
const text = textDestructuring.replace(/(\s*})$/, `, ${findByMethod}$1`);
allFixes.push(fixer.replaceText(allVariableDeclarations, text));
}
return allFixes;
},
});
},
};
},
});

View File

@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-presence-queries';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
docs: {
description: 'Ensure appropriate `get*`/`query*` queries are used with their respective matchers',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
wrongPresenceQuery: 'Use `getBy*` queries rather than `queryBy*` for checking element is present',
wrongAbsenceQuery: 'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
presence: {
type: 'boolean',
},
absence: {
type: 'boolean',
},
},
},
],
type: 'suggestion',
},
defaultOptions: [
{
presence: true,
absence: true,
},
],
create(context, [{ absence = true, presence = true }], helpers) {
return {
'CallExpression Identifier'(node) {
const expectCallNode = (0, node_utils_1.findClosestCallNode)(node, 'expect');
const withinCallNode = (0, node_utils_1.findClosestCallNode)(node, 'within');
if (!expectCallNode || !(0, node_utils_1.isMemberExpression)(expectCallNode.parent)) {
return;
}
if (!helpers.isSyncQuery(node)) {
return;
}
const isPresenceQuery = helpers.isGetQueryVariant(node);
const expectStatement = expectCallNode.parent;
const isPresenceAssert = helpers.isPresenceAssert(expectStatement);
const isAbsenceAssert = helpers.isAbsenceAssert(expectStatement);
if (!isPresenceAssert && !isAbsenceAssert) {
return;
}
if (presence &&
(withinCallNode || isPresenceAssert) &&
!isPresenceQuery) {
context.report({ node, messageId: 'wrongPresenceQuery' });
}
else if (!withinCallNode &&
absence &&
isAbsenceAssert &&
isPresenceQuery) {
context.report({ node, messageId: 'wrongAbsenceQuery' });
}
},
};
},
});

View File

@@ -0,0 +1,120 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-query-by-disappearance';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Suggest using `queryBy*` queries when waiting for disappearance',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
preferQueryByDisappearance: 'Prefer using queryBy* when waiting for disappearance',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function isWaitForElementToBeRemoved(node) {
const identifierNode = (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!identifierNode) {
return false;
}
return helpers.isAsyncUtil(identifierNode, ['waitForElementToBeRemoved']);
}
function reportExpression(node) {
const argumentProperty = (0, node_utils_1.isMemberExpression)(node)
? (0, node_utils_1.getPropertyIdentifierNode)(node.property)
: (0, node_utils_1.getPropertyIdentifierNode)(node);
if (!argumentProperty) {
return false;
}
if (helpers.isGetQueryVariant(argumentProperty) ||
helpers.isFindQueryVariant(argumentProperty)) {
context.report({
node: argumentProperty,
messageId: 'preferQueryByDisappearance',
});
return true;
}
return false;
}
function checkNonCallbackViolation(node) {
if (!(0, node_utils_1.isCallExpression)(node)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.callee) &&
!(0, node_utils_1.getPropertyIdentifierNode)(node.callee)) {
return false;
}
return reportExpression(node.callee);
}
function isReturnViolation(node) {
if (!(0, node_utils_1.isReturnStatement)(node) || !(0, node_utils_1.isCallExpression)(node.argument)) {
return false;
}
return reportExpression(node.argument.callee);
}
function isNonReturnViolation(node) {
if (!(0, node_utils_1.isExpressionStatement)(node) || !(0, node_utils_1.isCallExpression)(node.expression)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.expression.callee) &&
!(0, node_utils_1.getPropertyIdentifierNode)(node.expression.callee)) {
return false;
}
return reportExpression(node.expression.callee);
}
function isStatementViolation(statement) {
return isReturnViolation(statement) || isNonReturnViolation(statement);
}
function checkFunctionExpressionViolation(node) {
if (!(0, node_utils_1.isFunctionExpression)(node)) {
return false;
}
return node.body.body.some((statement) => isStatementViolation(statement));
}
function isArrowFunctionBodyViolation(node) {
if (!(0, node_utils_1.isArrowFunctionExpression)(node) || !(0, node_utils_1.isBlockStatement)(node.body)) {
return false;
}
return node.body.body.some((statement) => isStatementViolation(statement));
}
function isArrowFunctionImplicitReturnViolation(node) {
if (!(0, node_utils_1.isArrowFunctionExpression)(node) || !(0, node_utils_1.isCallExpression)(node.body)) {
return false;
}
if (!(0, node_utils_1.isMemberExpression)(node.body.callee) &&
!(0, node_utils_1.getPropertyIdentifierNode)(node.body.callee)) {
return false;
}
return reportExpression(node.body.callee);
}
function checkArrowFunctionViolation(node) {
return (isArrowFunctionBodyViolation(node) ||
isArrowFunctionImplicitReturnViolation(node));
}
function check(node) {
if (!isWaitForElementToBeRemoved(node)) {
return;
}
const argumentNode = node.arguments[0];
checkNonCallbackViolation(argumentNode);
checkArrowFunctionViolation(argumentNode);
checkFunctionExpressionViolation(argumentNode);
}
return {
CallExpression: check,
};
},
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-query-matchers';
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
docs: {
description: 'Ensure the configured `get*`/`query*` query is used with the corresponding matchers',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
wrongQueryForMatcher: 'Use `{{ query }}By*` queries for {{ matcher }}',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
validEntries: {
type: 'array',
items: {
type: 'object',
properties: {
query: {
type: 'string',
enum: ['get', 'query'],
},
matcher: {
type: 'string',
},
},
},
},
},
},
],
type: 'suggestion',
},
defaultOptions: [
{
validEntries: [],
},
],
create(context, [{ validEntries }], helpers) {
return {
'CallExpression Identifier'(node) {
const expectCallNode = (0, node_utils_1.findClosestCallNode)(node, 'expect');
if (!expectCallNode || !(0, node_utils_1.isMemberExpression)(expectCallNode.parent)) {
return;
}
if (!helpers.isSyncQuery(node)) {
return;
}
const isGetBy = helpers.isGetQueryVariant(node);
const expectStatement = expectCallNode.parent;
for (const entry of validEntries) {
const { query, matcher } = entry;
const isMatchingAssertForThisEntry = helpers.isMatchingAssert(expectStatement, matcher);
if (!isMatchingAssertForThisEntry) {
continue;
}
const actualQuery = isGetBy ? 'get' : 'query';
if (query !== actualQuery) {
context.report({
node,
messageId: 'wrongQueryForMatcher',
data: { query, matcher },
});
}
}
},
};
},
});

View File

@@ -0,0 +1,132 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-screen-queries';
const ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING = [
'container',
'baseElement',
];
function usesContainerOrBaseElement(node) {
const secondArgument = node.arguments[1];
return ((0, node_utils_1.isObjectExpression)(secondArgument) &&
secondArgument.properties.some((property) => (0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
ALLOWED_RENDER_PROPERTIES_FOR_DESTRUCTURING.includes(property.key.name)));
}
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using `screen` while querying',
recommendedConfig: {
dom: 'error',
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
preferScreenQueries: 'Avoid destructuring queries from `render` result, use `screen.{{ name }}` instead',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const renderWrapperNames = [];
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
function isReportableRender(node) {
return (helpers.isRenderUtil(node) || renderWrapperNames.includes(node.name));
}
function reportInvalidUsage(node) {
context.report({
node,
messageId: 'preferScreenQueries',
data: {
name: node.name,
},
});
}
function saveSafeDestructuredQueries(node) {
if ((0, node_utils_1.isObjectPattern)(node.id)) {
for (const property of node.id.properties) {
if ((0, node_utils_1.isProperty)(property) &&
utils_1.ASTUtils.isIdentifier(property.key) &&
helpers.isBuiltInQuery(property.key)) {
safeDestructuredQueries.push(property.key.name);
}
}
}
}
function isIdentifierAllowed(name) {
return ['screen', ...withinDeclaredVariables].includes(name);
}
const safeDestructuredQueries = [];
const withinDeclaredVariables = [];
return {
VariableDeclarator(node) {
if (!(0, node_utils_1.isCallExpression)(node.init) ||
!utils_1.ASTUtils.isIdentifier(node.init.callee)) {
return;
}
const isComingFromValidRender = isReportableRender(node.init.callee);
if (!isComingFromValidRender) {
saveSafeDestructuredQueries(node);
}
const isWithinFunction = node.init.callee.name === 'within';
const usesRenderOptions = isComingFromValidRender && usesContainerOrBaseElement(node.init);
if (!isWithinFunction && !usesRenderOptions) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
saveSafeDestructuredQueries(node);
}
else if (utils_1.ASTUtils.isIdentifier(node.id)) {
withinDeclaredVariables.push(node.id.name);
}
},
CallExpression(node) {
const identifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!identifierNode) {
return;
}
if (helpers.isRenderUtil(identifierNode)) {
detectRenderWrapper(identifierNode);
}
if (!helpers.isBuiltInQuery(identifierNode)) {
return;
}
if (!(0, node_utils_1.isMemberExpression)(identifierNode.parent)) {
const isSafeDestructuredQuery = safeDestructuredQueries.some((queryName) => queryName === identifierNode.name);
if (isSafeDestructuredQuery) {
return;
}
reportInvalidUsage(identifierNode);
return;
}
const memberExpressionNode = identifierNode.parent;
if ((0, node_utils_1.isCallExpression)(memberExpressionNode.object) &&
utils_1.ASTUtils.isIdentifier(memberExpressionNode.object.callee) &&
memberExpressionNode.object.callee.name !== 'within' &&
isReportableRender(memberExpressionNode.object.callee) &&
!usesContainerOrBaseElement(memberExpressionNode.object)) {
reportInvalidUsage(identifierNode);
return;
}
if (utils_1.ASTUtils.isIdentifier(memberExpressionNode.object) &&
!isIdentifierAllowed(memberExpressionNode.object.name)) {
reportInvalidUsage(identifierNode);
}
},
};
},
});

View File

@@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAPPING_TO_USER_EVENT = exports.UserEventMethods = exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-user-event';
exports.UserEventMethods = [
'click',
'dblClick',
'type',
'upload',
'clear',
'selectOptions',
'deselectOptions',
'tab',
'hover',
'unhover',
'paste',
];
exports.MAPPING_TO_USER_EVENT = {
click: ['click', 'type', 'selectOptions', 'deselectOptions'],
change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'],
dblClick: ['dblClick'],
input: ['type', 'upload', 'selectOptions', 'deselectOptions', 'paste'],
keyDown: ['type', 'tab'],
keyPress: ['type'],
keyUp: ['type', 'tab'],
mouseDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
mouseEnter: ['hover', 'selectOptions', 'deselectOptions'],
mouseLeave: ['unhover'],
mouseMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'],
mouseOut: ['unhover'],
mouseOver: ['hover', 'selectOptions', 'deselectOptions'],
mouseUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
paste: ['paste'],
pointerDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
pointerEnter: ['hover', 'selectOptions', 'deselectOptions'],
pointerLeave: ['unhover'],
pointerMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'],
pointerOut: ['unhover'],
pointerOver: ['hover', 'selectOptions', 'deselectOptions'],
pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
};
function buildErrorMessage(fireEventMethod) {
const userEventMethods = exports.MAPPING_TO_USER_EVENT[fireEventMethod].map((methodName) => `userEvent.${methodName}`);
return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1');
}
const fireEventMappedMethods = Object.keys(exports.MAPPING_TO_USER_EVENT);
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Suggest using `userEvent` over `fireEvent` for simulating user interactions',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
preferUserEvent: 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}',
},
schema: [
{
type: 'object',
properties: {
allowedMethods: { type: 'array' },
},
},
],
},
defaultOptions: [{ allowedMethods: [] }],
create(context, [options], helpers) {
const { allowedMethods } = options;
const createEventVariables = {};
const isfireEventMethodAllowed = (methodName) => !fireEventMappedMethods.includes(methodName) ||
allowedMethods.includes(methodName);
const getFireEventMethodName = (callExpressionNode, node) => {
if (!utils_1.ASTUtils.isIdentifier(callExpressionNode.callee) &&
!(0, node_utils_1.isMemberExpression)(callExpressionNode.callee)) {
return node.name;
}
const secondArgument = callExpressionNode.arguments[1];
if (utils_1.ASTUtils.isIdentifier(secondArgument) &&
createEventVariables[secondArgument.name] !== undefined) {
return createEventVariables[secondArgument.name];
}
if (!(0, node_utils_1.isCallExpression)(secondArgument) ||
!helpers.isCreateEventUtil(secondArgument)) {
return node.name;
}
if (utils_1.ASTUtils.isIdentifier(secondArgument.callee)) {
return secondArgument.arguments[0]
.value;
}
return secondArgument.callee
.property.name;
};
return {
'CallExpression Identifier'(node) {
if (!helpers.isFireEventMethod(node)) {
return;
}
const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
if (!closestCallExpression) {
return;
}
const fireEventMethodName = getFireEventMethodName(closestCallExpression, node);
if (!fireEventMethodName ||
isfireEventMethodAllowed(fireEventMethodName)) {
return;
}
context.report({
node: closestCallExpression.callee,
messageId: 'preferUserEvent',
data: {
userEventMethods: buildErrorMessage(fireEventMethodName),
fireEventMethod: fireEventMethodName,
},
});
},
VariableDeclarator(node) {
if (!(0, node_utils_1.isCallExpression)(node.init) ||
!helpers.isCreateEventUtil(node.init) ||
!utils_1.ASTUtils.isIdentifier(node.id)) {
return;
}
let fireEventMethodName = '';
if ((0, node_utils_1.isMemberExpression)(node.init.callee) &&
utils_1.ASTUtils.isIdentifier(node.init.callee.property)) {
fireEventMethodName = node.init.callee.property.name;
}
else if (node.init.arguments.length > 0) {
fireEventMethodName = node.init.arguments[0]
.value;
}
if (!isfireEventMethodAllowed(fireEventMethodName)) {
createEventVariables[node.id.name] = fireEventMethodName;
}
},
};
},
});

View File

@@ -0,0 +1,146 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'prefer-wait-for';
const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange'];
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Use `waitFor` instead of deprecated wait methods',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
marko: false,
},
},
messages: {
preferWaitForMethod: '`{{ methodName }}` is deprecated in favour of `waitFor`',
preferWaitForImport: 'import `waitFor` instead of deprecated async utils',
preferWaitForRequire: 'require `waitFor` instead of deprecated async utils',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
let addWaitFor = false;
const reportRequire = (node) => {
context.report({
node,
messageId: 'preferWaitForRequire',
fix(fixer) {
const excludedImports = [...DEPRECATED_METHODS, 'waitFor'];
const newAllRequired = node.properties
.filter((s) => (0, node_utils_1.isProperty)(s) &&
utils_1.ASTUtils.isIdentifier(s.key) &&
!excludedImports.includes(s.key.name))
.map((s) => s.key.name);
newAllRequired.push('waitFor');
return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`);
},
});
};
const reportImport = (node) => {
context.report({
node,
messageId: 'preferWaitForImport',
fix(fixer) {
const excludedImports = [...DEPRECATED_METHODS, 'waitFor'];
const newImports = node.specifiers
.map((specifier) => (0, node_utils_1.isImportSpecifier)(specifier) &&
!excludedImports.includes(specifier.imported.name) &&
specifier.imported.name)
.filter(Boolean);
newImports.push('waitFor');
const newNode = `import { ${newImports.join(',')} } from '${node.source.value}';`;
return fixer.replaceText(node, newNode);
},
});
};
const reportWait = (node) => {
context.report({
node,
messageId: 'preferWaitForMethod',
data: {
methodName: node.name,
},
fix(fixer) {
const callExpressionNode = (0, node_utils_1.findClosestCallExpressionNode)(node);
if (!callExpressionNode) {
return null;
}
const [arg] = callExpressionNode.arguments;
const fixers = [];
if (arg) {
fixers.push(fixer.replaceText(node, 'waitFor'));
if (node.name === 'waitForDomChange') {
fixers.push(fixer.insertTextBefore(arg, '() => {}, '));
}
}
else {
let methodReplacement = 'waitFor(() => {})';
if ((0, node_utils_1.isMemberExpression)(node.parent) &&
utils_1.ASTUtils.isIdentifier(node.parent.object)) {
methodReplacement = `${node.parent.object.name}.${methodReplacement}`;
}
const newText = methodReplacement;
fixers.push(fixer.replaceText(callExpressionNode, newText));
}
return fixers;
},
});
};
return {
'CallExpression > MemberExpression'(node) {
const isDeprecatedMethod = utils_1.ASTUtils.isIdentifier(node.property) &&
DEPRECATED_METHODS.includes(node.property.name);
if (!isDeprecatedMethod) {
return;
}
if (!helpers.isNodeComingFromTestingLibrary(node)) {
return;
}
addWaitFor = true;
reportWait(node.property);
},
'CallExpression > Identifier'(node) {
if (!DEPRECATED_METHODS.includes(node.name)) {
return;
}
if (!helpers.isNodeComingFromTestingLibrary(node)) {
return;
}
addWaitFor = true;
reportWait(node);
},
'Program:exit'() {
var _a;
if (!addWaitFor) {
return;
}
const testingLibraryNode = (_a = helpers.getCustomModuleImportNode()) !== null && _a !== void 0 ? _a : helpers.getTestingLibraryImportNode();
if ((0, node_utils_1.isCallExpression)(testingLibraryNode)) {
const parent = testingLibraryNode.parent;
if (!(0, node_utils_1.isObjectPattern)(parent.id)) {
return;
}
reportRequire(parent.id);
}
else if (testingLibraryNode) {
if (testingLibraryNode.specifiers.length === 1 &&
(0, node_utils_1.isImportNamespaceSpecifier)(testingLibraryNode.specifiers[0])) {
return;
}
reportImport(testingLibraryNode);
}
},
};
},
});

View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RULE_NAME = void 0;
const utils_1 = require("@typescript-eslint/utils");
const create_testing_library_rule_1 = require("../create-testing-library-rule");
const node_utils_1 = require("../node-utils");
exports.RULE_NAME = 'render-result-naming-convention';
const ALLOWED_VAR_NAMES = ['view', 'utils'];
const ALLOWED_VAR_NAMES_TEXT = ALLOWED_VAR_NAMES.map((name) => `\`${name}\``)
.join(', ')
.replace(/, ([^,]*)$/, ', or $1');
exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
name: exports.RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce a valid naming for return value from `render`',
recommendedConfig: {
dom: false,
angular: 'error',
react: 'error',
vue: 'error',
marko: 'error',
},
},
messages: {
renderResultNamingConvention: `\`{{ renderResultName }}\` is not a recommended name for \`render\` returned value. Instead, you should destructure it, or name it using one of: ${ALLOWED_VAR_NAMES_TEXT}`,
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
const renderWrapperNames = [];
function detectRenderWrapper(node) {
const innerFunction = (0, node_utils_1.getInnermostReturningFunction)(context, node);
if (innerFunction) {
renderWrapperNames.push((0, node_utils_1.getFunctionName)(innerFunction));
}
}
return {
CallExpression(node) {
const callExpressionIdentifier = (0, node_utils_1.getDeepestIdentifierNode)(node);
if (!callExpressionIdentifier) {
return;
}
if (helpers.isRenderUtil(callExpressionIdentifier)) {
detectRenderWrapper(callExpressionIdentifier);
}
},
VariableDeclarator(node) {
if (!node.init) {
return;
}
const initIdentifierNode = (0, node_utils_1.getDeepestIdentifierNode)(node.init);
if (!initIdentifierNode) {
return;
}
if (!helpers.isRenderVariableDeclarator(node) &&
!renderWrapperNames.includes(initIdentifierNode.name)) {
return;
}
if ((0, node_utils_1.isObjectPattern)(node.id)) {
return;
}
const renderResultName = utils_1.ASTUtils.isIdentifier(node.id) && node.id.name;
if (!renderResultName) {
return;
}
const isAllowedRenderResultName = ALLOWED_VAR_NAMES.includes(renderResultName);
if (isAllowedRenderResultName) {
return;
}
context.report({
node,
messageId: 'renderResultNamingConvention',
data: {
renderResultName,
},
});
},
};
},
});