88 isArrowFunctionExpression ,
99 isBlockStatement ,
1010 isCallExpression ,
11+ isFunctionDeclaration ,
1112 isFunctionExpression ,
1213 isMemberExpression ,
1314} from '../node-utils' ;
@@ -18,10 +19,54 @@ const RULE_NAME = 'no-unsettled-absence-query';
1819export type MessageIds = 'noUnsettledAbsenceQuery' ;
1920export type Options = [ ] ;
2021
21- // Matchers that indicate absence when negated, beyond those already
22- // covered by helpers.isAbsenceAssert() (which handles PRESENCE_MATCHERS).
2322const NEGATED_ABSENCE_MATCHERS = [ 'toBeVisible' ] ;
2423
24+ function isNestedFunction ( node : TSESTree . Node ) : boolean {
25+ return (
26+ isArrowFunctionExpression ( node ) ||
27+ isFunctionExpression ( node ) ||
28+ isFunctionDeclaration ( node )
29+ ) ;
30+ }
31+
32+ function containsNode (
33+ node : TSESTree . Node ,
34+ predicate : ( n : TSESTree . Node ) => boolean
35+ ) : boolean {
36+ if ( predicate ( node ) ) {
37+ return true ;
38+ }
39+
40+ if ( isNestedFunction ( node ) ) {
41+ return false ;
42+ }
43+
44+ for ( const key of Object . keys ( node ) ) {
45+ if ( key === 'parent' ) continue ;
46+ const child = ( node as unknown as Record < string , unknown > ) [ key ] ;
47+ if ( child && typeof child === 'object' ) {
48+ if ( Array . isArray ( child ) ) {
49+ for ( const item of child ) {
50+ if (
51+ item &&
52+ typeof item === 'object' &&
53+ 'type' in item &&
54+ containsNode ( item as TSESTree . Node , predicate )
55+ ) {
56+ return true ;
57+ }
58+ }
59+ } else if (
60+ 'type' in child &&
61+ containsNode ( child as TSESTree . Node , predicate )
62+ ) {
63+ return true ;
64+ }
65+ }
66+ }
67+ return false ;
68+ }
69+
2570export default createTestingLibraryRule < Options , MessageIds > ( {
2671 name : RULE_NAME ,
2772 meta : {
@@ -62,12 +107,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
62107 ) ;
63108 }
64109
65- /**
66- * Determines whether a node is inside a callback passed to an async
67- * Testing Library utility (e.g. waitFor). Absence assertions inside
68- * these callbacks are always flagged because they can pass on the first
69- * invocation before the component has settled.
70- */
71110 function isInsideAsyncUtilCallback ( node : TSESTree . Node ) : boolean {
72111 let current : TSESTree . Node | undefined = node . parent ;
73112
@@ -121,82 +160,23 @@ export default createTestingLibraryRule<Options, MessageIds>({
121160 return null ;
122161 }
123162
124- function containsAwaitExpression ( node : TSESTree . Node ) : boolean {
125- if ( ASTUtils . isAwaitExpression ( node ) ) {
126- return true ;
127- }
128-
129- for ( const key of Object . keys ( node ) ) {
130- if ( key === 'parent' ) continue ;
131- const child = ( node as unknown as Record < string , unknown > ) [ key ] ;
132- if ( child && typeof child === 'object' ) {
133- if ( Array . isArray ( child ) ) {
134- for ( const item of child ) {
135- if (
136- item &&
137- typeof item === 'object' &&
138- 'type' in item &&
139- containsAwaitExpression ( item as TSESTree . Node )
140- ) {
141- return true ;
142- }
143- }
144- } else if (
145- 'type' in child &&
146- containsAwaitExpression ( child as TSESTree . Node )
147- ) {
148- return true ;
149- }
150- }
151- }
152- return false ;
153- }
154-
155- function containsGetQueryCall ( node : TSESTree . Node ) : boolean {
156- if ( ASTUtils . isIdentifier ( node ) && helpers . isGetQueryVariant ( node ) ) {
157- return true ;
158- }
159-
160- for ( const key of Object . keys ( node ) ) {
161- if ( key === 'parent' ) continue ;
162- const child = ( node as unknown as Record < string , unknown > ) [ key ] ;
163- if ( child && typeof child === 'object' ) {
164- if ( Array . isArray ( child ) ) {
165- for ( const item of child ) {
166- if (
167- item &&
168- typeof item === 'object' &&
169- 'type' in item &&
170- containsGetQueryCall ( item as TSESTree . Node )
171- ) {
172- return true ;
173- }
174- }
175- } else if (
176- 'type' in child &&
177- containsGetQueryCall ( child as TSESTree . Node )
178- ) {
179- return true ;
180- }
181- }
182- }
183- return false ;
184- }
185-
186163 function hasSettlingExpression ( statement : TSESTree . Statement ) : boolean {
187- return (
188- containsAwaitExpression ( statement ) || containsGetQueryCall ( statement )
164+ const hasAwait = containsNode ( statement , ( n ) =>
165+ ASTUtils . isAwaitExpression ( n )
166+ ) ;
167+ const hasGetQuery = containsNode (
168+ statement ,
169+ ( n ) => ASTUtils . isIdentifier ( n ) && helpers . isGetQueryVariant ( n )
189170 ) ;
171+ return hasAwait || hasGetQuery ;
190172 }
191173
192174 return {
193175 'CallExpression Identifier' ( node : TSESTree . Identifier ) {
194- // Only interested in queryBy* / queryAllBy* variants
195176 if ( ! helpers . isQueryQueryVariant ( node ) ) {
196177 return ;
197178 }
198179
199- // Must be inside an expect() call
200180 const expectCallNode = findClosestCallNode ( node , 'expect' ) ;
201181 if (
202182 ! expectCallNode ?. parent ||
@@ -205,14 +185,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
205185 return ;
206186 }
207187
208- // Must be an absence assertion
209188 if ( ! isAbsenceAssertion ( expectCallNode . parent ) ) {
210189 return ;
211190 }
212191
213- // Absence assertions inside async util callbacks (e.g. waitFor) are
214- // always flagged - they pass on the first invocation before the
215- // component has settled.
216192 if ( isInsideAsyncUtilCallback ( node ) ) {
217193 context . report ( {
218194 node,
@@ -222,8 +198,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
222198 return ;
223199 }
224200
225- // Find the enclosing function body and determine whether a settling
226- // expression appears on any preceding statement.
227201 const functionBody = findEnclosingFunctionBody ( node ) ;
228202 if ( ! functionBody ) {
229203 return ;
0 commit comments