Skip to content

Commit

Permalink
feat(await-async-events): instance of userEvent is recognized as async
Browse files Browse the repository at this point in the history
feat(await-async-events): added comments

feat(await-async-events): better test case

feat(await-async-events): edge case fixed, test added

feat(await-async-events): use actual userEvent import for check, tests
  • Loading branch information
Kvanttinen committed Oct 16, 2023
1 parent b531af8 commit 8ba464f
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 39 deletions.
83 changes: 49 additions & 34 deletions lib/create-testing-library-rule/detect-testing-library-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ type IsAsyncUtilFn = (
validNames?: readonly (typeof ASYNC_UTILS)[number][]
) => boolean;
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean;
type IsUserEventMethodFn = (
node: TSESTree.Identifier,
userEventSession?: string
) => boolean;
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
type IsCreateEventUtil = (
node: TSESTree.CallExpression | TSESTree.Identifier
Expand All @@ -97,6 +100,9 @@ type FindImportedTestingLibraryUtilSpecifierFn = (
type IsNodeComingFromTestingLibraryFn = (
node: TSESTree.Identifier | TSESTree.MemberExpression
) => boolean;
type getUserEventImportIdentifierFn = (
node: ImportModuleNode | null
) => TSESTree.Identifier | null;

export interface DetectionHelpers {
getTestingLibraryImportNode: GetTestingLibraryImportNodeFn;
Expand Down Expand Up @@ -130,6 +136,7 @@ export interface DetectionHelpers {
canReportErrors: CanReportErrorsFn;
findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn;
isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn;
getUserEventImportIdentifier: getUserEventImportIdentifierFn;
}

const USER_EVENT_PACKAGE = '@testing-library/user-event';
Expand Down Expand Up @@ -326,6 +333,35 @@ export function detectTestingLibraryUtils<
return getImportModuleName(importedCustomModuleNode);
};

const getUserEventImportIdentifier = (node: ImportModuleNode | null) => {
if (!node) {
return null;
}

if (isImportDeclaration(node)) {
const userEventIdentifier = node.specifiers.find((specifier) =>
isImportDefaultSpecifier(specifier)
);

if (userEventIdentifier) {
return userEventIdentifier.local;
}
} else {
if (!ASTUtils.isVariableDeclarator(node.parent)) {
return null;
}

const requireNode = node.parent;
if (!ASTUtils.isIdentifier(requireNode.id)) {
return null;
}

return requireNode.id;
}

return null;
};

/**
* Determines whether Testing Library utils are imported or not for
* current file being analyzed.
Expand Down Expand Up @@ -557,7 +593,10 @@ export function detectTestingLibraryUtils<
return regularCall || wildcardCall || wildcardCallWithCallExpression;
};

const isUserEventMethod: IsUserEventMethodFn = (node) => {
const isUserEventMethod: IsUserEventMethodFn = (
node,
userEventInstance
) => {
const userEvent = findImportedUserEventSpecifier();
let userEventName: string | undefined;

Expand All @@ -567,7 +606,7 @@ export function detectTestingLibraryUtils<
userEventName = USER_EVENT_NAME;
}

if (!userEventName) {
if (!userEventName && !userEventInstance) {
return false;
}

Expand All @@ -591,8 +630,11 @@ export function detectTestingLibraryUtils<

// check userEvent.click() usage
return (
ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventName
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventName) ||
// check userEventInstance.click() usage
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventInstance)
);
};

Expand Down Expand Up @@ -853,35 +895,7 @@ export function detectTestingLibraryUtils<

const findImportedUserEventSpecifier: () => TSESTree.Identifier | null =
() => {
if (!importedUserEventLibraryNode) {
return null;
}

if (isImportDeclaration(importedUserEventLibraryNode)) {
const userEventIdentifier =
importedUserEventLibraryNode.specifiers.find((specifier) =>
isImportDefaultSpecifier(specifier)
);

if (userEventIdentifier) {
return userEventIdentifier.local;
}
} else {
if (
!ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent)
) {
return null;
}

const requireNode = importedUserEventLibraryNode.parent;
if (!ASTUtils.isIdentifier(requireNode.id)) {
return null;
}

return requireNode.id;
}

return null;
return getUserEventImportIdentifier(importedUserEventLibraryNode);
};

const getTestingLibraryImportedUtilSpecifier = (
Expand Down Expand Up @@ -997,6 +1011,7 @@ export function detectTestingLibraryUtils<
canReportErrors,
findImportedTestingLibraryUtilSpecifier,
isNodeComingFromTestingLibrary,
getUserEventImportIdentifier,
};

// Instructions for Testing Library detection.
Expand Down
34 changes: 34 additions & 0 deletions lib/node-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,37 @@ export function findImportSpecifier(
return (property as TSESTree.Property).key as TSESTree.Identifier;
}
}

/**
* Finds if the userEvent is used as an instance
*/

export function getUserEventInstance(
context: TSESLint.RuleContext<string, unknown[]>,
userEventImport: TSESTree.Identifier | null
): string | undefined {
const { tokensAndComments } = context.getSourceCode();
if (!userEventImport) {
return undefined;
}
/**
* Check for the following pattern:
* userEvent.setup(
* For a line like this:
* const user = userEvent.setup();
* function will return 'user'
*/
for (const [index, token] of tokensAndComments.entries()) {
if (
token.type === 'Identifier' &&
token.value === userEventImport.name &&
tokensAndComments[index + 1].value === '.' &&
tokensAndComments[index + 2].value === 'setup' &&
tokensAndComments[index + 3].value === '(' &&
tokensAndComments[index - 1].value === '='
) {
return tokensAndComments[index - 2].value;
}
}
return undefined;
}
21 changes: 17 additions & 4 deletions lib/rules/await-async-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
findClosestFunctionExpressionNode,
getFunctionName,
getInnermostReturningFunction,
getUserEventInstance,
getVariableReferences,
isMemberExpression,
isPromiseHandled,
Expand Down Expand Up @@ -91,9 +92,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
messageId?: MessageIds;
fix?: TSESLint.ReportFixFunction;
}): void {
if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
return;
}
if (!isPromiseHandled(node)) {
context.report({
node: closestCallExpression.callee,
Expand Down Expand Up @@ -121,9 +119,20 @@ export default createTestingLibraryRule<Options, MessageIds>({

return {
'CallExpression Identifier'(node: TSESTree.Identifier) {
const importedUserEventLibraryNode =
helpers.getTestingLibraryImportNode();
const userEventImport = helpers.getUserEventImportIdentifier(
importedUserEventLibraryNode
);
// Check if userEvent is used as an instance, like const user = userEvent.setup()
const userEventInstance = getUserEventInstance(
context,
userEventImport
);
if (
(isFireEventEnabled && helpers.isFireEventMethod(node)) ||
(isUserEventEnabled && helpers.isUserEventMethod(node))
(isUserEventEnabled &&
helpers.isUserEventMethod(node, userEventInstance))
) {
detectEventMethodWrapper(node);

Expand All @@ -136,6 +145,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
return;
}

if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
return;
}

const references = getVariableReferences(
context,
closestCallExpression.parent
Expand Down
Loading

0 comments on commit 8ba464f

Please sign in to comment.