Skip to content

feat: add support for fireEvent as function in prefer-user-event #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/rules/prefer-user-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ fireEventAliased.click(node);
import * as dom from '@testing-library/dom';
// or const dom = require(@testing-library/dom');
dom.fireEvent.click(node);

// using fireEvent as a function
import * as dom from '@testing-library/dom';
dom.fireEvent(node, dom.createEvent('click', node));

import { fireEvent, createEvent } from '@testing-library/dom';
const clickEvent = createEvent.click(node);
fireEvent(node, clickEvent);
```

Examples of **correct** code for this rule:
Expand All @@ -48,6 +56,9 @@ fireEvent.cut(node);
import * as dom from '@testing-library/dom';
// or const dom = require('@testing-library/dom');
dom.fireEvent.cut(node);

import { fireEvent, createEvent } from '@testing-library/dom';
fireEvent(node, createEvent('cut', node));
```

#### Options
Expand Down
99 changes: 81 additions & 18 deletions lib/create-testing-library-rule/detect-testing-library-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ type IsAsyncUtilFn = (
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean;
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
type IsCreateEventUtil = (
node: TSESTree.CallExpression | TSESTree.Identifier
) => boolean;
type IsRenderVariableDeclaratorFn = (
node: TSESTree.VariableDeclarator
) => boolean;
Expand Down Expand Up @@ -115,6 +118,7 @@ export interface DetectionHelpers {
isFireEventMethod: IsFireEventMethodFn;
isUserEventMethod: IsUserEventMethodFn;
isRenderUtil: IsRenderUtilFn;
isCreateEventUtil: IsCreateEventUtil;
isRenderVariableDeclarator: IsRenderVariableDeclaratorFn;
isDebugUtil: IsDebugUtilFn;
isActUtil: (node: TSESTree.Identifier) => boolean;
Expand All @@ -128,6 +132,7 @@ export interface DetectionHelpers {
const USER_EVENT_PACKAGE = '@testing-library/user-event';
const REACT_DOM_TEST_UTILS_PACKAGE = 'react-dom/test-utils';
const FIRE_EVENT_NAME = 'fireEvent';
const CREATE_EVENT_NAME = 'createEvent';
const USER_EVENT_NAME = 'userEvent';
const RENDER_NAME = 'render';

Expand Down Expand Up @@ -471,6 +476,7 @@ export function detectTestingLibraryUtils<
/**
* Determines whether a given node is fireEvent method or not
*/
// eslint-disable-next-line complexity
const isFireEventMethod: IsFireEventMethodFn = (node) => {
const fireEventUtil =
findImportedTestingLibraryUtilSpecifier(FIRE_EVENT_NAME);
Expand All @@ -493,33 +499,54 @@ export function detectTestingLibraryUtils<
? node.parent
: undefined;

if (!parentMemberExpression) {
const parentCallExpression: TSESTree.CallExpression | undefined =
node.parent && isCallExpression(node.parent) ? node.parent : undefined;

if (!parentMemberExpression && !parentCallExpression) {
return false;
}

// make sure that given node it's not fireEvent object itself
if (
[fireEventUtilName, FIRE_EVENT_NAME].includes(node.name) ||
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === node.name)
) {
return false;
// check fireEvent('method', node) usage
if (parentCallExpression) {
return [fireEventUtilName, FIRE_EVENT_NAME].includes(node.name);
}

// we know it's defined at this point, but TS seems to think it is not
// so here I'm enforcing it once in order to avoid using "!" operator every time
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const definedParentMemberExpression = parentMemberExpression!;

// check fireEvent.click() usage
const regularCall =
ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === fireEventUtilName;
ASTUtils.isIdentifier(definedParentMemberExpression.object) &&
isCallExpression(definedParentMemberExpression.parent) &&
definedParentMemberExpression.object.name === fireEventUtilName &&
node.name !== FIRE_EVENT_NAME &&
node.name !== fireEventUtilName;

// check testingLibraryUtils.fireEvent.click() usage
const wildcardCall =
isMemberExpression(parentMemberExpression.object) &&
ASTUtils.isIdentifier(parentMemberExpression.object.object) &&
parentMemberExpression.object.object.name === fireEventUtilName &&
ASTUtils.isIdentifier(parentMemberExpression.object.property) &&
parentMemberExpression.object.property.name === FIRE_EVENT_NAME;

return regularCall || wildcardCall;
isMemberExpression(definedParentMemberExpression.object) &&
ASTUtils.isIdentifier(definedParentMemberExpression.object.object) &&
definedParentMemberExpression.object.object.name ===
fireEventUtilName &&
ASTUtils.isIdentifier(definedParentMemberExpression.object.property) &&
definedParentMemberExpression.object.property.name ===
FIRE_EVENT_NAME &&
node.name !== FIRE_EVENT_NAME &&
node.name !== fireEventUtilName;

// check testingLibraryUtils.fireEvent('click')
const wildcardCallWithCallExpression =
ASTUtils.isIdentifier(definedParentMemberExpression.object) &&
definedParentMemberExpression.object.name === fireEventUtilName &&
ASTUtils.isIdentifier(definedParentMemberExpression.property) &&
definedParentMemberExpression.property.name === FIRE_EVENT_NAME &&
!isMemberExpression(definedParentMemberExpression.parent) &&
node.name === FIRE_EVENT_NAME &&
node.name !== fireEventUtilName;

return regularCall || wildcardCall || wildcardCallWithCallExpression;
};

const isUserEventMethod: IsUserEventMethodFn = (node) => {
Expand Down Expand Up @@ -595,6 +622,40 @@ export function detectTestingLibraryUtils<
}
);

const isCreateEventUtil: IsCreateEventUtil = (node) => {
const isCreateEventCallback = (
identifierNodeName: string,
originalNodeName?: string
) => [identifierNodeName, originalNodeName].includes(CREATE_EVENT_NAME);
if (
isCallExpression(node) &&
isMemberExpression(node.callee) &&
ASTUtils.isIdentifier(node.callee.object)
) {
return isPotentialTestingLibraryFunction(
node.callee.object,
isCreateEventCallback
);
}

if (
isCallExpression(node) &&
isMemberExpression(node.callee) &&
isMemberExpression(node.callee.object) &&
ASTUtils.isIdentifier(node.callee.object.property)
) {
return isPotentialTestingLibraryFunction(
node.callee.object.property,
isCreateEventCallback
);
}
const identifier = getDeepestIdentifierNode(node);
return isPotentialTestingLibraryFunction(
identifier,
isCreateEventCallback
);
};

const isRenderVariableDeclarator: IsRenderVariableDeclaratorFn = (node) => {
if (!node.init) {
return false;
Expand Down Expand Up @@ -712,7 +773,8 @@ export function detectTestingLibraryUtils<
isRenderUtil(node) ||
isFireEventMethod(node) ||
isUserEventMethod(node) ||
isActUtil(node)
isActUtil(node) ||
isCreateEventUtil(node)
);
};

Expand Down Expand Up @@ -906,6 +968,7 @@ export function detectTestingLibraryUtils<
isFireEventMethod,
isUserEventMethod,
isRenderUtil,
isCreateEventUtil,
isRenderVariableDeclarator,
isDebugUtil,
isActUtil,
Expand Down
83 changes: 75 additions & 8 deletions lib/rules/prefer-user-event.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils';

import { createTestingLibraryRule } from '../create-testing-library-rule';
import { findClosestCallExpressionNode } from '../node-utils';
import {
findClosestCallExpressionNode,
isCallExpression,
isMemberExpression,
} from '../node-utils';

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

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

create(context, [options], helpers) {
const { allowedMethods } = options;

const createEventVariables: Record<string, string | undefined> = {};

const isfireEventMethodAllowed = (methodName: string) =>
!fireEventMappedMethods.includes(methodName) ||
allowedMethods.includes(methodName);

const getFireEventMethodName = (
callExpressionNode: TSESTree.CallExpression,
node: TSESTree.Identifier
) => {
if (
!ASTUtils.isIdentifier(callExpressionNode.callee) &&
!isMemberExpression(callExpressionNode.callee)
) {
return node.name;
}
const secondArgument = callExpressionNode.arguments[1];
if (
ASTUtils.isIdentifier(secondArgument) &&
createEventVariables[secondArgument.name] !== undefined
) {
return createEventVariables[secondArgument.name];
}
if (
!isCallExpression(secondArgument) ||
!helpers.isCreateEventUtil(secondArgument)
) {
return node.name;
}
if (ASTUtils.isIdentifier(secondArgument.callee)) {
// createEvent('click', foo)
return (secondArgument.arguments[0] as TSESTree.Literal)
.value as string;
}
// createEvent.click(foo)
return (
(secondArgument.callee as TSESTree.MemberExpression)
.property as TSESTree.Identifier
).name;
};
return {
'CallExpression Identifier'(node: TSESTree.Identifier) {
if (!helpers.isFireEventMethod(node)) {
return;
}

const closestCallExpression = findClosestCallExpressionNode(node, true);

if (!closestCallExpression) {
return;
}

const fireEventMethodName: string = node.name;
const fireEventMethodName = getFireEventMethodName(
closestCallExpression,
node
);

if (
!fireEventMappedMethods.includes(fireEventMethodName) ||
allowedMethods.includes(fireEventMethodName)
!fireEventMethodName ||
isfireEventMethodAllowed(fireEventMethodName)
) {
return;
}

context.report({
node: closestCallExpression.callee,
messageId: 'preferUserEvent',
Expand All @@ -122,6 +166,29 @@ export default createTestingLibraryRule<Options, MessageIds>({
},
});
},

VariableDeclarator(node: TSESTree.VariableDeclarator) {
if (
!isCallExpression(node.init) ||
!helpers.isCreateEventUtil(node.init) ||
!ASTUtils.isIdentifier(node.id)
) {
return;
}
let fireEventMethodName = '';
if (
isMemberExpression(node.init.callee) &&
ASTUtils.isIdentifier(node.init.callee.property)
) {
fireEventMethodName = node.init.callee.property.name;
} else {
fireEventMethodName = (node.init.arguments[0] as TSESTree.Literal)
.value as string;
}
if (!isfireEventMethodAllowed(fireEventMethodName)) {
createEventVariables[node.id.name] = fireEventMethodName;
}
},
};
},
});
Loading