Skip to content

Commit d3b4fb2

Browse files
authored
Merge branch 'main' into fix/no-node-access-assignment-case
2 parents 9146a8d + 92a6bf5 commit d3b4fb2

File tree

7 files changed

+147
-32
lines changed

7 files changed

+147
-32
lines changed

lib/rules/await-async-utils.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,29 @@ export default createTestingLibraryRule<Options, MessageIds>({
121121
functionWrappersNames.push((node.id as TSESTree.Identifier).name);
122122
}
123123
},
124-
'CallExpression Identifier'(node: TSESTree.Identifier) {
124+
CallExpression(node: TSESTree.CallExpression) {
125+
const callExpressionIdentifier = getDeepestIdentifierNode(node);
126+
127+
if (!callExpressionIdentifier) {
128+
return;
129+
}
130+
125131
const isAsyncUtilOrKnownAliasAroundIt =
126-
helpers.isAsyncUtil(node) ||
127-
functionWrappersNames.includes(node.name);
132+
helpers.isAsyncUtil(callExpressionIdentifier) ||
133+
functionWrappersNames.includes(callExpressionIdentifier.name);
128134
if (!isAsyncUtilOrKnownAliasAroundIt) {
129135
return;
130136
}
131137

132138
// detect async query used within wrapper function for later analysis
133-
if (helpers.isAsyncUtil(node)) {
134-
detectAsyncUtilWrapper(node);
139+
if (helpers.isAsyncUtil(callExpressionIdentifier)) {
140+
detectAsyncUtilWrapper(callExpressionIdentifier);
135141
}
136142

137-
const closestCallExpression = findClosestCallExpressionNode(node, true);
143+
const closestCallExpression = findClosestCallExpressionNode(
144+
callExpressionIdentifier,
145+
true
146+
);
138147

139148
if (!closestCallExpression?.parent) {
140149
return;
@@ -146,12 +155,12 @@ export default createTestingLibraryRule<Options, MessageIds>({
146155
);
147156

148157
if (references.length === 0) {
149-
if (!isPromiseHandled(node)) {
158+
if (!isPromiseHandled(callExpressionIdentifier)) {
150159
context.report({
151-
node,
152-
messageId: getMessageId(node),
160+
node: callExpressionIdentifier,
161+
messageId: getMessageId(callExpressionIdentifier),
153162
data: {
154-
name: node.name,
163+
name: callExpressionIdentifier.name,
155164
},
156165
});
157166
}
@@ -160,10 +169,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
160169
const referenceNode = reference.identifier as TSESTree.Identifier;
161170
if (!isPromiseHandled(referenceNode)) {
162171
context.report({
163-
node,
164-
messageId: getMessageId(node),
172+
node: callExpressionIdentifier,
173+
messageId: getMessageId(callExpressionIdentifier),
165174
data: {
166-
name: node.name,
175+
name: callExpressionIdentifier.name,
167176
},
168177
});
169178
return;

lib/rules/no-node-access.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import {
2+
DefinitionType,
3+
type ScopeVariable,
4+
} from '@typescript-eslint/scope-manager';
15
import { TSESTree, ASTUtils } from '@typescript-eslint/utils';
26

37
import { createTestingLibraryRule } from '../create-testing-library-rule';
4-
import { isCallExpression, isMemberExpression } from '../node-utils';
8+
import {
9+
getDeepestIdentifierNode,
10+
getPropertyIdentifierNode,
11+
isCallExpression,
12+
isMemberExpression,
13+
} from '../node-utils';
514
import {
615
ALL_RETURNING_NODES,
716
EVENT_HANDLER_METHODS,
17+
getScope,
818
resolveToTestingLibraryFn,
919
} from '../utils';
1020

@@ -89,24 +99,39 @@ export default createTestingLibraryRule<Options, MessageIds>({
8999
}
90100
}
91101

102+
function detectTestingLibraryFn(
103+
node: TSESTree.CallExpression,
104+
variable: ScopeVariable | null
105+
) {
106+
if (variable && variable.defs.length > 0) {
107+
const def = variable.defs[0];
108+
if (
109+
def.type === DefinitionType.Variable &&
110+
isCallExpression(def.node.init)
111+
) {
112+
return resolveToTestingLibraryFn(def.node.init, context);
113+
}
114+
}
115+
116+
return resolveToTestingLibraryFn(node, context);
117+
}
118+
92119
return {
93120
CallExpression(node: TSESTree.CallExpression) {
94-
const { callee } = node;
95-
const property = isMemberExpression(callee) ? callee.property : null;
96-
const object = isMemberExpression(callee) ? callee.object : null;
97-
98-
const propertyName = ASTUtils.isIdentifier(property)
99-
? property.name
100-
: null;
101-
const objectName = ASTUtils.isIdentifier(object) ? object.name : null;
121+
const property = getDeepestIdentifierNode(node);
122+
const identifier = getPropertyIdentifierNode(node);
102123

103124
const isEventHandlerMethod = EVENT_HANDLER_METHODS.some(
104-
(method) => method === propertyName
125+
(method) => method === property?.name
105126
);
106127
const hasUserEventInstanceName = userEventInstanceNames.has(
107-
objectName ?? ''
128+
identifier?.name ?? ''
108129
);
109-
const testingLibraryFn = resolveToTestingLibraryFn(node, context);
130+
131+
const variable = identifier
132+
? ASTUtils.findVariable(getScope(context, node), identifier)
133+
: null;
134+
const testingLibraryFn = detectTestingLibraryFn(node, variable);
110135

111136
if (
112137
!testingLibraryFn &&

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"is-ci": "^3.0.1",
8383
"jest": "^29.7.0",
8484
"lint-staged": "^15.2.10",
85-
"prettier": "3.3.3",
85+
"prettier": "3.6.2",
8686
"semantic-release": "^24.2.0",
8787
"semver": "^7.6.3",
8888
"ts-node": "^10.9.2",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/lib/rules/await-async-queries.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ ruleTester.run(RULE_NAME, rule, {
113113
testingFramework: '@marko/testing-library',
114114
}),
115115

116+
// async queries not called are valid
117+
...createTestCase((query) => `expect(screen.${query}).toBeDefined()`, {
118+
isAsync: false,
119+
}),
120+
116121
// async queries are valid with await operator
117122
...createTestCase(
118123
(query) => `

tests/lib/rules/await-async-utils.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ ruleTester.run(RULE_NAME, rule, {
3131
const aPromise = ${asyncUtil}(() => getByLabelText('email'));
3232
await aPromise;
3333
});
34+
`,
35+
})),
36+
...ASYNC_UTILS.map((asyncUtil) => ({
37+
code: `
38+
import { ${asyncUtil} } from '${testingFramework}';
39+
test('${asyncUtil} util not called is valid', () => {
40+
expect(${asyncUtil}).toBeDefined();
41+
});
3442
`,
3543
})),
3644
...ASYNC_UTILS.map((asyncUtil) => ({
@@ -286,6 +294,23 @@ ruleTester.run(RULE_NAME, rule, {
286294
test('edge case for no innermost function scope', () => {
287295
const foo = waitFor
288296
})
297+
`,
298+
},
299+
{
300+
// edge case for coverage: CallExpressions without deepest identifiers
301+
code: `
302+
import { waitFor } from '${testingFramework}';
303+
test('coverage test for CallExpressions without identifiers', () => {
304+
const asyncUtil = waitFor
305+
306+
// These CallExpressions have no deepest identifier:
307+
const funcs = [() => console.log('test')]
308+
const obj = { [Symbol.iterator]: () => 'symbol' }
309+
310+
funcs[0]()
311+
obj[Symbol.iterator]()
312+
(function() { return 'iife' })()
313+
});
289314
`,
290315
},
291316
...ASYNC_UTILS.map((asyncUtil) => ({

tests/lib/rules/no-node-access.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ ruleTester.run(RULE_NAME, rule, {
181181
const buttonText = screen.getByText('submit');
182182
const userAlias = userEvent.setup();
183183
userAlias.click(buttonText);
184+
`,
185+
},
186+
{
187+
code: `
188+
import userEvent from '@testing-library/user-event';
189+
import { screen } from '${testingFramework}';
190+
test('...', () => {
191+
const buttonText = screen.getByText('submit');
192+
(() => { click: userEvent.click(buttonText); })();
193+
});
184194
`,
185195
},
186196
{
@@ -278,7 +288,6 @@ ruleTester.run(RULE_NAME, rule, {
278288
},
279289
{
280290
code: `
281-
// case: custom module set but not imported using ${testingFramework} (aggressive reporting limited)
282291
import { screen } from '${testingFramework}';
283292
284293
const ui = {
@@ -288,6 +297,48 @@ ruleTester.run(RULE_NAME, rule, {
288297
const select = ui.select.get();
289298
expect(select).toHaveClass(selectClasses.select);
290299
});
300+
`,
301+
},
302+
{
303+
settings: { 'testing-library/utils-module': 'test-utils' },
304+
code: `
305+
// case: custom module set but not imported using ${testingFramework} (aggressive reporting limited)
306+
import { screen, render } from 'test-utils';
307+
import MyComponent from './MyComponent'
308+
309+
test('...', async () => {
310+
const { user } = render(<MyComponent />)
311+
await user.click(screen.getByRole("button"))
312+
});
313+
`,
314+
},
315+
{
316+
settings: { 'testing-library/utils-module': 'test-utils' },
317+
code: `
318+
// case: custom module set but not imported using ${testingFramework} (aggressive reporting limited)
319+
import { screen, render } from 'test-utils';
320+
import MyComponent from './MyComponent'
321+
322+
test('...', async () => {
323+
const result = render(<MyComponent />)
324+
await result.user.click(screen.getByRole("button"))
325+
});
326+
`,
327+
},
328+
{
329+
settings: {
330+
'testing-library/utils-module': 'TestUtils',
331+
'testing-library/custom-renders': ['renderComponent'],
332+
},
333+
code: `
334+
// case: custom module set but not imported using ${testingFramework} (aggressive reporting limited)
335+
import { screen, renderComponent } from './TestUtils';
336+
import MyComponent from './MyComponent'
337+
338+
test('...', async () => {
339+
const result = renderComponent(<MyComponent />)
340+
await result.user.click(screen.getByRole("button"))
341+
});
291342
`,
292343
},
293344
]

0 commit comments

Comments
 (0)