Skip to content

Commit

Permalink
feat(await-async-events): instance of userEvent is recognized as async
Browse files Browse the repository at this point in the history
feat(await-async-events): added comments

feat(await-async-events): better test case

feat(await-async-events): edge case fixed, test added

feat(await-async-events): use actual userEvent import for check, tests
  • Loading branch information
Kvanttinen authored and Belco90 committed Jan 12, 2024
1 parent 27dfa51 commit 636dee8
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 36 deletions.
83 changes: 49 additions & 34 deletions lib/create-testing-library-rule/detect-testing-library-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ type IsAsyncUtilFn = (
validNames?: readonly (typeof ASYNC_UTILS)[number][]
) => boolean;
type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean;
type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean;
type IsUserEventMethodFn = (
node: TSESTree.Identifier,
userEventSession?: string
) => boolean;
type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean;
type IsCreateEventUtil = (
node: TSESTree.CallExpression | TSESTree.Identifier
Expand All @@ -97,6 +100,9 @@ type FindImportedTestingLibraryUtilSpecifierFn = (
type IsNodeComingFromTestingLibraryFn = (
node: TSESTree.Identifier | TSESTree.MemberExpression
) => boolean;
type getUserEventImportIdentifierFn = (
node: ImportModuleNode | null
) => TSESTree.Identifier | null;

export interface DetectionHelpers {
getTestingLibraryImportNode: GetTestingLibraryImportNodeFn;
Expand Down Expand Up @@ -130,6 +136,7 @@ export interface DetectionHelpers {
canReportErrors: CanReportErrorsFn;
findImportedTestingLibraryUtilSpecifier: FindImportedTestingLibraryUtilSpecifierFn;
isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn;
getUserEventImportIdentifier: getUserEventImportIdentifierFn;
}

const USER_EVENT_PACKAGE = '@testing-library/user-event';
Expand Down Expand Up @@ -326,6 +333,35 @@ export function detectTestingLibraryUtils<
return getImportModuleName(importedCustomModuleNode);
};

const getUserEventImportIdentifier = (node: ImportModuleNode | null) => {
if (!node) {
return null;
}

if (isImportDeclaration(node)) {
const userEventIdentifier = node.specifiers.find((specifier) =>
isImportDefaultSpecifier(specifier)
);

if (userEventIdentifier) {
return userEventIdentifier.local;
}
} else {
if (!ASTUtils.isVariableDeclarator(node.parent)) {
return null;
}

const requireNode = node.parent;
if (!ASTUtils.isIdentifier(requireNode.id)) {
return null;
}

return requireNode.id;
}

return null;
};

/**
* Determines whether Testing Library utils are imported or not for
* current file being analyzed.
Expand Down Expand Up @@ -557,7 +593,10 @@ export function detectTestingLibraryUtils<
return regularCall || wildcardCall || wildcardCallWithCallExpression;
};

const isUserEventMethod: IsUserEventMethodFn = (node) => {
const isUserEventMethod: IsUserEventMethodFn = (
node,
userEventInstance
) => {
const userEvent = findImportedUserEventSpecifier();
let userEventName: string | undefined;

Expand All @@ -567,7 +606,7 @@ export function detectTestingLibraryUtils<
userEventName = USER_EVENT_NAME;
}

if (!userEventName) {
if (!userEventName && !userEventInstance) {
return false;
}

Expand All @@ -591,8 +630,11 @@ export function detectTestingLibraryUtils<

// check userEvent.click() usage
return (
ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventName
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventName) ||
// check userEventInstance.click() usage
(ASTUtils.isIdentifier(parentMemberExpression.object) &&
parentMemberExpression.object.name === userEventInstance)
);
};

Expand Down Expand Up @@ -853,35 +895,7 @@ export function detectTestingLibraryUtils<

const findImportedUserEventSpecifier: () => TSESTree.Identifier | null =
() => {
if (!importedUserEventLibraryNode) {
return null;
}

if (isImportDeclaration(importedUserEventLibraryNode)) {
const userEventIdentifier =
importedUserEventLibraryNode.specifiers.find((specifier) =>
isImportDefaultSpecifier(specifier)
);

if (userEventIdentifier) {
return userEventIdentifier.local;
}
} else {
if (
!ASTUtils.isVariableDeclarator(importedUserEventLibraryNode.parent)
) {
return null;
}

const requireNode = importedUserEventLibraryNode.parent;
if (!ASTUtils.isIdentifier(requireNode.id)) {
return null;
}

return requireNode.id;
}

return null;
return getUserEventImportIdentifier(importedUserEventLibraryNode);
};

const getTestingLibraryImportedUtilSpecifier = (
Expand Down Expand Up @@ -997,6 +1011,7 @@ export function detectTestingLibraryUtils<
canReportErrors,
findImportedTestingLibraryUtilSpecifier,
isNodeComingFromTestingLibrary,
getUserEventImportIdentifier,
};

// Instructions for Testing Library detection.
Expand Down
34 changes: 34 additions & 0 deletions lib/node-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,37 @@ export function findImportSpecifier(
return (property as TSESTree.Property).key as TSESTree.Identifier;
}
}

/**
* Finds if the userEvent is used as an instance
*/

export function getUserEventInstance(
context: TSESLint.RuleContext<string, unknown[]>,
userEventImport: TSESTree.Identifier | null
): string | undefined {
const { tokensAndComments } = context.getSourceCode();
if (!userEventImport) {
return undefined;
}
/**
* Check for the following pattern:
* userEvent.setup(
* For a line like this:
* const user = userEvent.setup();
* function will return 'user'
*/
for (const [index, token] of tokensAndComments.entries()) {
if (
token.type === 'Identifier' &&
token.value === userEventImport.name &&
tokensAndComments[index + 1].value === '.' &&
tokensAndComments[index + 2].value === 'setup' &&
tokensAndComments[index + 3].value === '(' &&
tokensAndComments[index - 1].value === '='
) {
return tokensAndComments[index - 2].value;
}
}
return undefined;
}
14 changes: 13 additions & 1 deletion lib/rules/await-async-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
findClosestFunctionExpressionNode,
getFunctionName,
getInnermostReturningFunction,
getUserEventInstance,
getVariableReferences,
isMemberExpression,
isPromiseHandled,
Expand Down Expand Up @@ -118,9 +119,20 @@ export default createTestingLibraryRule<Options, MessageIds>({

return {
'CallExpression Identifier'(node: TSESTree.Identifier) {
const importedUserEventLibraryNode =
helpers.getTestingLibraryImportNode();
const userEventImport = helpers.getUserEventImportIdentifier(
importedUserEventLibraryNode
);
// Check if userEvent is used as an instance, like const user = userEvent.setup()
const userEventInstance = getUserEventInstance(
context,
userEventImport
);
if (
(isFireEventEnabled && helpers.isFireEventMethod(node)) ||
(isUserEventEnabled && helpers.isUserEventMethod(node))
(isUserEventEnabled &&
helpers.isUserEventMethod(node, userEventInstance))
) {
detectEventMethodWrapper(node);

Expand Down
141 changes: 140 additions & 1 deletion tests/lib/rules/await-async-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const USER_EVENT_ASYNC_FUNCTIONS = [
'upload',
] as const;
const FIRE_EVENT_ASYNC_FRAMEWORKS = [
'@testing-library/vue',
// '@testing-library/vue',
'@marko/testing-library',
] as const;
const USER_EVENT_ASYNC_FRAMEWORKS = ['@testing-library/user-event'] as const;
Expand Down Expand Up @@ -374,6 +374,27 @@ ruleTester.run(RULE_NAME, rule, {
`,
options: [{ eventModule: ['userEvent', 'fireEvent'] }] as Options,
},
{
code: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('userEvent as instance', async () => {
const user = userEvent.setup()
await user.click(getByLabelText('username'))
})
`,
options: [{ eventModule: ['userEvent'] }] as Options,
},
{
code: `
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('userEvent as named import', async () => {
const user = u.setup()
await user.click(getByLabelText('username'))
await u.click(getByLabelText('username'))
})
`,
options: [{ eventModule: ['userEvent'] }] as Options,
},
]),
],

Expand Down Expand Up @@ -960,6 +981,70 @@ ruleTester.run(RULE_NAME, rule, {
}
triggerEvent()
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
(eventMethod) =>
({
code: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('instance of userEvent is recognized as async event', async function() {
const user = userEvent.setup()
user.${eventMethod}(getByLabelText('username'))
})
`,
errors: [
{
line: 5,
column: 5,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('instance of userEvent is recognized as async event', async function() {
const user = userEvent.setup()
await user.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
...USER_EVENT_ASYNC_FUNCTIONS.map(
(eventMethod) =>
({
code: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
const user = userEvent.setup()
user.${eventMethod}(getByLabelText('username'))
userEvent.${eventMethod}(getByLabelText('username'))
})
`,
errors: [
{
line: 5,
column: 5,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
{
line: 6,
column: 5,
messageId: 'awaitAsyncEvent',
data: { name: eventMethod },
},
],
options: [{ eventModule: 'userEvent' }],
output: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('instance of userEvent is recognized as async event along with static userEvent', async function() {
const user = userEvent.setup()
await user.${eventMethod}(getByLabelText('username'))
await userEvent.${eventMethod}(getByLabelText('username'))
})
`,
} as const)
),
Expand Down Expand Up @@ -1021,6 +1106,60 @@ ruleTester.run(RULE_NAME, rule, {
fireEvent.click(getByLabelText('username'))
await userEvent.click(getByLabelText('username'))
})
`,
},
{
code: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
let user;
beforeEach(() => {
user = userEvent.setup()
})
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
user.click(getByLabelText('username'))
})
`,
errors: [
{
line: 8,
column: 5,
messageId: 'awaitAsyncEvent',
data: { name: 'click' },
},
],
output: `
import userEvent from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
let user;
beforeEach(() => {
user = userEvent.setup()
})
test('instance of userEvent is recognized as async event when instance is initialized in beforeEach', async function() {
await user.click(getByLabelText('username'))
})
`,
},
{
code: `
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('userEvent as named import', async function() {
const user = u.setup()
user.click(getByLabelText('username'))
})
`,
errors: [
{
line: 5,
column: 5,
messageId: 'awaitAsyncEvent',
data: { name: 'click' },
},
],
output: `
import u from '${USER_EVENT_ASYNC_FRAMEWORKS[0]}'
test('userEvent as named import', async function() {
const user = u.setup()
await user.click(getByLabelText('username'))
})
`,
},
],
Expand Down

0 comments on commit 636dee8

Please sign in to comment.