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
  • Loading branch information
Kvanttinen committed Oct 15, 2023
1 parent b531af8 commit f28f7e1
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 10 deletions.
19 changes: 14 additions & 5 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 Down Expand Up @@ -557,7 +560,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 +573,7 @@ export function detectTestingLibraryUtils<
userEventName = USER_EVENT_NAME;
}

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

Expand All @@ -591,8 +597,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
30 changes: 30 additions & 0 deletions lib/node-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,33 @@ 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[]>
): string | undefined {
const { tokensAndComments } = context.getSourceCode();
/**
* 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 === 'userEvent' &&
tokensAndComments[index + 1].value === '.' &&
tokensAndComments[index + 2].value === 'setup' &&
tokensAndComments[index + 3].value === '(' &&
tokensAndComments[index - 1].value === '='
) {
return tokensAndComments[index - 2].value;
}
}
return undefined;
}
13 changes: 9 additions & 4 deletions 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 @@ -91,9 +92,6 @@ export default createTestingLibraryRule<Options, MessageIds>({
messageId?: MessageIds;
fix?: TSESLint.ReportFixFunction;
}): void {
if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
return;
}
if (!isPromiseHandled(node)) {
context.report({
node: closestCallExpression.callee,
Expand Down Expand Up @@ -121,9 +119,12 @@ export default createTestingLibraryRule<Options, MessageIds>({

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

Expand All @@ -136,6 +137,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
return;
}

if (node.name === USER_EVENT_SETUP_FUNCTION_NAME) {
return;
}

const references = getVariableReferences(
context,
closestCallExpression.parent
Expand Down
106 changes: 105 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 @@ -361,6 +361,16 @@ 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,
},
]),
],

Expand Down Expand Up @@ -947,6 +957,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 @@ -1008,6 +1082,36 @@ 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'))
})
`,
},
],
Expand Down

0 comments on commit f28f7e1

Please sign in to comment.