Skip to content

Commit c907890

Browse files
committed
feat(no-unnecessary-act): first approach for the rule
1 parent 0276efb commit c907890

File tree

4 files changed

+127
-0
lines changed

4 files changed

+127
-0
lines changed

docs/rules/no-unnecessary-act.md

Whitespace-only changes.

lib/create-testing-library-rule/detect-testing-library-utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface DetectionHelpers {
9696
getTestingLibraryImportName: GetTestingLibraryImportNameFn;
9797
getCustomModuleImportName: GetCustomModuleImportNameFn;
9898
isTestingLibraryImported: IsTestingLibraryImportedFn;
99+
isTestingLibraryUtil: (node: TSESTree.Identifier) => boolean;
99100
isGetQueryVariant: IsGetQueryVariantFn;
100101
isQueryQueryVariant: IsQueryQueryVariantFn;
101102
isFindQueryVariant: IsFindQueryVariantFn;
@@ -112,6 +113,7 @@ export interface DetectionHelpers {
112113
isRenderUtil: IsRenderUtilFn;
113114
isRenderVariableDeclarator: IsRenderVariableDeclaratorFn;
114115
isDebugUtil: IsDebugUtilFn;
116+
isActUtil: (node: TSESTree.Identifier) => boolean;
115117
isPresenceAssert: IsPresenceAssertFn;
116118
isAbsenceAssert: IsAbsenceAssertFn;
117119
canReportErrors: CanReportErrorsFn;
@@ -618,6 +620,30 @@ export function detectTestingLibraryUtils<
618620
);
619621
};
620622

623+
const isActUtil = (node: TSESTree.Identifier): boolean => {
624+
return isPotentialTestingLibraryFunction(
625+
node,
626+
(identifierNodeName, originalNodeName) => {
627+
return [identifierNodeName, originalNodeName]
628+
.filter(Boolean)
629+
.includes('act');
630+
}
631+
);
632+
633+
// TODO: check if `act` coming from 'react-dom/test-utils'
634+
};
635+
636+
const isTestingLibraryUtil = (node: TSESTree.Identifier): boolean => {
637+
return (
638+
isAsyncUtil(node) ||
639+
isQuery(node) ||
640+
isRenderUtil(node) ||
641+
isFireEventMethod(node) ||
642+
isUserEventMethod(node) ||
643+
isActUtil(node)
644+
);
645+
};
646+
621647
/**
622648
* Determines whether a given MemberExpression node is a presence assert
623649
*
@@ -795,6 +821,7 @@ export function detectTestingLibraryUtils<
795821
getTestingLibraryImportName,
796822
getCustomModuleImportName,
797823
isTestingLibraryImported,
824+
isTestingLibraryUtil,
798825
isGetQueryVariant,
799826
isQueryQueryVariant,
800827
isFindQueryVariant,
@@ -811,6 +838,7 @@ export function detectTestingLibraryUtils<
811838
isRenderUtil,
812839
isRenderVariableDeclarator,
813840
isDebugUtil,
841+
isActUtil,
814842
isPresenceAssert,
815843
isAbsenceAssert,
816844
canReportErrors,

lib/rules/no-unnecessary-act.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { createTestingLibraryRule } from '../create-testing-library-rule';
3+
import {
4+
getDeepestIdentifierNode,
5+
getPropertyIdentifierNode,
6+
isCallExpression,
7+
isExpressionStatement,
8+
} from '../node-utils';
9+
10+
export const RULE_NAME = 'no-unnecessary-act';
11+
export type MessageIds =
12+
| 'noUnnecessaryActTestingLibraryUtil'
13+
| 'noUnnecessaryActEmptyFunction';
14+
15+
export default createTestingLibraryRule<[], MessageIds>({
16+
name: RULE_NAME,
17+
meta: {
18+
type: 'problem',
19+
docs: {
20+
description:
21+
'Disallow the use of `act` when wrapping Testing Library utils or empty functions',
22+
category: 'Possible Errors',
23+
recommended: false,
24+
},
25+
messages: {
26+
noUnnecessaryActTestingLibraryUtil:
27+
'Avoid wrapping Testing Library util calls in `act`',
28+
noUnnecessaryActEmptyFunction: 'Avoid wrapping empty function in `act`',
29+
},
30+
schema: [],
31+
},
32+
defaultOptions: [],
33+
34+
create(context, _, helpers) {
35+
function hasNonTestingLibraryCall(
36+
statements: TSESTree.Statement[]
37+
): boolean {
38+
for (const statement of statements) {
39+
if (!isExpressionStatement(statement)) {
40+
continue;
41+
}
42+
43+
if (!isCallExpression(statement.expression)) {
44+
continue;
45+
}
46+
47+
const identifier = getDeepestIdentifierNode(statement.expression);
48+
49+
if (!identifier) {
50+
continue;
51+
}
52+
53+
if (helpers.isTestingLibraryUtil(identifier)) {
54+
continue;
55+
}
56+
57+
// at this point the statement is a non testing library call
58+
return true;
59+
}
60+
return false;
61+
}
62+
63+
function checkNoUnnecessaryAct(
64+
blockStatementNode: TSESTree.BlockStatement
65+
) {
66+
const callExpressionNode = blockStatementNode?.parent?.parent as
67+
| TSESTree.CallExpression
68+
| undefined;
69+
70+
if (!callExpressionNode) {
71+
return;
72+
}
73+
74+
const callExpressionIdentifier = getPropertyIdentifierNode(
75+
callExpressionNode
76+
);
77+
78+
if (!callExpressionIdentifier) {
79+
return;
80+
}
81+
82+
if (!helpers.isActUtil(callExpressionIdentifier)) {
83+
return;
84+
}
85+
86+
// TODO: check if empty function body
87+
88+
if (hasNonTestingLibraryCall(blockStatementNode.body)) {
89+
return;
90+
}
91+
}
92+
93+
return {
94+
'CallExpression > ArrowFunctionExpression > BlockStatement': checkNoUnnecessaryAct,
95+
'CallExpression > FunctionExpression > BlockStatement': checkNoUnnecessaryAct,
96+
// TODO: add selector for call expression > arrow function > implicit return
97+
};
98+
},
99+
});

tests/lib/rules/no-unnecessary-act.test.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)