Skip to content

Commit 0c11511

Browse files
authored
feat(prefer-user-event): add support for fireEvent as function (#398)
* feat: add support for fireEvent as function in prefer-user-event * refactor: use getDeepestIdentifierNode to detect in isCreateEventUtil Co-authored-by: Mario Beltrán Alarcón <[email protected]> Closes #261
1 parent 5623f96 commit 0c11511

File tree

4 files changed

+360
-26
lines changed

4 files changed

+360
-26
lines changed

docs/rules/prefer-user-event.md

+11
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ fireEventAliased.click(node);
2929
import * as dom from '@testing-library/dom';
3030
// or const dom = require(@testing-library/dom');
3131
dom.fireEvent.click(node);
32+
33+
// using fireEvent as a function
34+
import * as dom from '@testing-library/dom';
35+
dom.fireEvent(node, dom.createEvent('click', node));
36+
37+
import { fireEvent, createEvent } from '@testing-library/dom';
38+
const clickEvent = createEvent.click(node);
39+
fireEvent(node, clickEvent);
3240
```
3341

3442
Examples of **correct** code for this rule:
@@ -48,6 +56,9 @@ fireEvent.cut(node);
4856
import * as dom from '@testing-library/dom';
4957
// or const dom = require('@testing-library/dom');
5058
dom.fireEvent.cut(node);
59+
60+
import { fireEvent, createEvent } from '@testing-library/dom';
61+
fireEvent(node, createEvent('cut', node));
5162
```
5263

5364
#### Options

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

+81-18
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ type IsAsyncUtilFn = (
7777
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
7878
type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean;
7979
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
80+
type IsCreateEventUtil = (
81+
node: TSESTree.CallExpression | TSESTree.Identifier
82+
) => boolean;
8083
type IsRenderVariableDeclaratorFn = (
8184
node: TSESTree.VariableDeclarator
8285
) => boolean;
@@ -115,6 +118,7 @@ export interface DetectionHelpers {
115118
isFireEventMethod: IsFireEventMethodFn;
116119
isUserEventMethod: IsUserEventMethodFn;
117120
isRenderUtil: IsRenderUtilFn;
121+
isCreateEventUtil: IsCreateEventUtil;
118122
isRenderVariableDeclarator: IsRenderVariableDeclaratorFn;
119123
isDebugUtil: IsDebugUtilFn;
120124
isActUtil: (node: TSESTree.Identifier) => boolean;
@@ -128,6 +132,7 @@ export interface DetectionHelpers {
128132
const USER_EVENT_PACKAGE = '@testing-library/user-event';
129133
const REACT_DOM_TEST_UTILS_PACKAGE = 'react-dom/test-utils';
130134
const FIRE_EVENT_NAME = 'fireEvent';
135+
const CREATE_EVENT_NAME = 'createEvent';
131136
const USER_EVENT_NAME = 'userEvent';
132137
const RENDER_NAME = 'render';
133138

@@ -471,6 +476,7 @@ export function detectTestingLibraryUtils<
471476
/**
472477
* Determines whether a given node is fireEvent method or not
473478
*/
479+
// eslint-disable-next-line complexity
474480
const isFireEventMethod: IsFireEventMethodFn = (node) => {
475481
const fireEventUtil =
476482
findImportedTestingLibraryUtilSpecifier(FIRE_EVENT_NAME);
@@ -493,33 +499,54 @@ export function detectTestingLibraryUtils<
493499
? node.parent
494500
: undefined;
495501

496-
if (!parentMemberExpression) {
502+
const parentCallExpression: TSESTree.CallExpression | undefined =
503+
node.parent && isCallExpression(node.parent) ? node.parent : undefined;
504+
505+
if (!parentMemberExpression && !parentCallExpression) {
497506
return false;
498507
}
499508

500-
// make sure that given node it's not fireEvent object itself
501-
if (
502-
[fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) ||
503-
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
504-
parentMemberExpression.object.name === node.name)
505-
) {
506-
return false;
509+
// check fireEvent('method', node) usage
510+
if (parentCallExpression) {
511+
return [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name);
507512
}
508513

514+
// we know it's defined at this point, but TS seems to think it is not
515+
// so here I'm enforcing it once in order to avoid using "!" operator every time
516+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
517+
const definedParentMemberExpression = parentMemberExpression!;
518+
509519
// check fireEvent.click() usage
510520
const regularCall =
511-
ASTUtils.isIdentifier(parentMemberExpression.object) &&
512-
parentMemberExpression.object.name === fireEventUtilName;
521+
ASTUtils.isIdentifier(definedParentMemberExpression.object) &&
522+
isCallExpression(definedParentMemberExpression.parent) &&
523+
definedParentMemberExpression.object.name === fireEventUtilName &&
524+
node.name !== FIRE_EVENT_NAME &&
525+
node.name !== fireEventUtilName;
513526

514527
// check testingLibraryUtils.fireEvent.click() usage
515528
const wildcardCall =
516-
isMemberExpression(parentMemberExpression.object) &&
517-
ASTUtils.isIdentifier(parentMemberExpression.object.object) &&
518-
parentMemberExpression.object.object.name === fireEventUtilName &&
519-
ASTUtils.isIdentifier(parentMemberExpression.object.property) &&
520-
parentMemberExpression.object.property.name === FIRE_EVENT_NAME;
521-
522-
return regularCall || wildcardCall;
529+
isMemberExpression(definedParentMemberExpression.object) &&
530+
ASTUtils.isIdentifier(definedParentMemberExpression.object.object) &&
531+
definedParentMemberExpression.object.object.name ===
532+
fireEventUtilName &&
533+
ASTUtils.isIdentifier(definedParentMemberExpression.object.property) &&
534+
definedParentMemberExpression.object.property.name ===
535+
FIRE_EVENT_NAME &&
536+
node.name !== FIRE_EVENT_NAME &&
537+
node.name !== fireEventUtilName;
538+
539+
// check testingLibraryUtils.fireEvent('click')
540+
const wildcardCallWithCallExpression =
541+
ASTUtils.isIdentifier(definedParentMemberExpression.object) &&
542+
definedParentMemberExpression.object.name === fireEventUtilName &&
543+
ASTUtils.isIdentifier(definedParentMemberExpression.property) &&
544+
definedParentMemberExpression.property.name === FIRE_EVENT_NAME &&
545+
!isMemberExpression(definedParentMemberExpression.parent) &&
546+
node.name === FIRE_EVENT_NAME &&
547+
node.name !== fireEventUtilName;
548+
549+
return regularCall || wildcardCall || wildcardCallWithCallExpression;
523550
};
524551

525552
const isUserEventMethod: IsUserEventMethodFn = (node) => {
@@ -595,6 +622,40 @@ export function detectTestingLibraryUtils<
595622
}
596623
);
597624

625+
const isCreateEventUtil: IsCreateEventUtil = (node) => {
626+
const isCreateEventCallback = (
627+
identifierNodeName: string,
628+
originalNodeName?: string
629+
) => [identifierNodeName, originalNodeName].includes(CREATE_EVENT_NAME);
630+
if (
631+
isCallExpression(node) &&
632+
isMemberExpression(node.callee) &&
633+
ASTUtils.isIdentifier(node.callee.object)
634+
) {
635+
return isPotentialTestingLibraryFunction(
636+
node.callee.object,
637+
isCreateEventCallback
638+
);
639+
}
640+
641+
if (
642+
isCallExpression(node) &&
643+
isMemberExpression(node.callee) &&
644+
isMemberExpression(node.callee.object) &&
645+
ASTUtils.isIdentifier(node.callee.object.property)
646+
) {
647+
return isPotentialTestingLibraryFunction(
648+
node.callee.object.property,
649+
isCreateEventCallback
650+
);
651+
}
652+
const identifier = getDeepestIdentifierNode(node);
653+
return isPotentialTestingLibraryFunction(
654+
identifier,
655+
isCreateEventCallback
656+
);
657+
};
658+
598659
const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => {
599660
if (!node.init) {
600661
return false;
@@ -712,7 +773,8 @@ export function detectTestingLibraryUtils<
712773
isRenderUtil(node) ||
713774
isFireEventMethod(node) ||
714775
isUserEventMethod(node) ||
715-
isActUtil(node)
776+
isActUtil(node) ||
777+
isCreateEventUtil(node)
716778
);
717779
};
718780

@@ -906,6 +968,7 @@ export function detectTestingLibraryUtils<
906968
isFireEventMethod,
907969
isUserEventMethod,
908970
isRenderUtil,
971+
isCreateEventUtil,
909972
isRenderVariableDeclarator,
910973
isDebugUtil,
911974
isActUtil,

lib/rules/prefer-user-event.ts

+75-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { TSESTree } from '@typescript-eslint/experimental-utils';
1+
import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils';
22

33
import { createTestingLibraryRule } from '../create-testing-library-rule';
4-
import { findClosestCallExpressionNode } from '../node-utils';
4+
import {
5+
findClosestCallExpressionNode,
6+
isCallExpression,
7+
isMemberExpression,
8+
} from '../node-utils';
59

610
export const RULE_NAME = 'prefer-user-event';
711

@@ -91,28 +95,68 @@ export default createTestingLibraryRule<Options, MessageIds>({
9195

9296
create(context, [options], helpers) {
9397
const { allowedMethods } = options;
94-
98+
const createEventVariables: Record<string, string | undefined> = {};
99+
100+
const isfireEventMethodAllowed = (methodName: string) =>
101+
!fireEventMappedMethods.includes(methodName) ||
102+
allowedMethods.includes(methodName);
103+
104+
const getFireEventMethodName = (
105+
callExpressionNode: TSESTree.CallExpression,
106+
node: TSESTree.Identifier
107+
) => {
108+
if (
109+
!ASTUtils.isIdentifier(callExpressionNode.callee) &&
110+
!isMemberExpression(callExpressionNode.callee)
111+
) {
112+
return node.name;
113+
}
114+
const secondArgument = callExpressionNode.arguments[1];
115+
if (
116+
ASTUtils.isIdentifier(secondArgument) &&
117+
createEventVariables[secondArgument.name] !== undefined
118+
) {
119+
return createEventVariables[secondArgument.name];
120+
}
121+
if (
122+
!isCallExpression(secondArgument) ||
123+
!helpers.isCreateEventUtil(secondArgument)
124+
) {
125+
return node.name;
126+
}
127+
if (ASTUtils.isIdentifier(secondArgument.callee)) {
128+
// createEvent('click', foo)
129+
return (secondArgument.arguments[0] as TSESTree.Literal)
130+
.value as string;
131+
}
132+
// createEvent.click(foo)
133+
return (
134+
(secondArgument.callee as TSESTree.MemberExpression)
135+
.property as TSESTree.Identifier
136+
).name;
137+
};
95138
return {
96139
'CallExpression Identifier'(node: TSESTree.Identifier) {
97140
if (!helpers.isFireEventMethod(node)) {
98141
return;
99142
}
100-
101143
const closestCallExpression = findClosestCallExpressionNode(node, true);
102144

103145
if (!closestCallExpression) {
104146
return;
105147
}
106148

107-
const fireEventMethodName: string = node.name;
149+
const fireEventMethodName = getFireEventMethodName(
150+
closestCallExpression,
151+
node
152+
);
108153

109154
if (
110-
!fireEventMappedMethods.includes(fireEventMethodName) ||
111-
allowedMethods.includes(fireEventMethodName)
155+
!fireEventMethodName ||
156+
isfireEventMethodAllowed(fireEventMethodName)
112157
) {
113158
return;
114159
}
115-
116160
context.report({
117161
node: closestCallExpression.callee,
118162
messageId: 'preferUserEvent',
@@ -122,6 +166,29 @@ export default createTestingLibraryRule<Options, MessageIds>({
122166
},
123167
});
124168
},
169+
170+
VariableDeclarator(node: TSESTree.VariableDeclarator) {
171+
if (
172+
!isCallExpression(node.init) ||
173+
!helpers.isCreateEventUtil(node.init) ||
174+
!ASTUtils.isIdentifier(node.id)
175+
) {
176+
return;
177+
}
178+
let fireEventMethodName = '';
179+
if (
180+
isMemberExpression(node.init.callee) &&
181+
ASTUtils.isIdentifier(node.init.callee.property)
182+
) {
183+
fireEventMethodName = node.init.callee.property.name;
184+
} else {
185+
fireEventMethodName = (node.init.arguments[0] as TSESTree.Literal)
186+
.value as string;
187+
}
188+
if (!isfireEventMethodAllowed(fireEventMethodName)) {
189+
createEventVariables[node.id.name] = fireEventMethodName;
190+
}
191+
},
125192
};
126193
},
127194
});

0 commit comments

Comments
 (0)