diff --git a/docs/rules/no-await-sync-events.md b/docs/rules/no-await-sync-events.md index 9eb61548..df95afd4 100644 --- a/docs/rules/no-await-sync-events.md +++ b/docs/rules/no-await-sync-events.md @@ -1,14 +1,15 @@ # Disallow unnecessary `await` for sync events (no-await-sync-events) -Ensure that sync events are not awaited unnecessarily. +Ensure that sync simulated events are not awaited unnecessarily. ## Rule Details -Functions in the event object provided by Testing Library, including -fireEvent and userEvent, do NOT return Promise, with an exception of -`userEvent.type`, which delays the promise resolve only if [`delay` +Methods for simulating events in Testing Library ecosystem -`fireEvent` and `userEvent`- +do NOT return any Promise, with an exception of +`userEvent.type` and `userEvent.keyboard`, which delays the promise resolve only if [`delay` option](https://github.com/testing-library/user-event#typeelement-text-options) is specified. -Some examples are: + +Some examples of simulating events not returning any Promise are: - `fireEvent.click` - `fireEvent.select` @@ -26,15 +27,16 @@ const foo = async () => { // ... }; -const bar = () => { +const bar = async () => { // ... await userEvent.tab(); // ... }; -const baz = () => { +const baz = async () => { // ... await userEvent.type(textInput, 'abc'); + await userEvent.keyboard('abc'); // ... }; ``` @@ -54,10 +56,14 @@ const bar = () => { // ... }; -const baz = () => { +const baz = async () => { // await userEvent.type only with delay option - await userEvent.type(textInput, 'abc', {delay: 1000}); + await userEvent.type(textInput, 'abc', { delay: 1000 }); userEvent.type(textInput, '123'); + + // same for userEvent.keyboard + await userEvent.keyboard(textInput, 'abc', { delay: 1000 }); + userEvent.keyboard('123'); // ... }; ``` diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index cc5686fa..63ea41b1 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -12,6 +12,7 @@ import { hasImportMatch, ImportModuleNode, isImportDeclaration, + isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isLiteral, @@ -20,9 +21,9 @@ import { } from './node-utils'; import { ABSENCE_MATCHERS, + ALL_QUERIES_COMBINATIONS, ASYNC_UTILS, PRESENCE_MATCHERS, - ALL_QUERIES_COMBINATIONS, } from './utils'; export type TestingLibrarySettings = { @@ -67,6 +68,7 @@ type IsAsyncUtilFn = ( validNames?: readonly typeof ASYNC_UTILS[number][] ) => boolean; type IsFireEventMethodFn = (node: TSESTree.Identifier) => boolean; +type IsUserEventMethodFn = (node: TSESTree.Identifier) => boolean; type IsRenderUtilFn = (node: TSESTree.Identifier) => boolean; type IsRenderVariableDeclaratorFn = ( node: TSESTree.VariableDeclarator @@ -99,6 +101,7 @@ export interface DetectionHelpers { isFireEventUtil: (node: TSESTree.Identifier) => boolean; isUserEventUtil: (node: TSESTree.Identifier) => boolean; isFireEventMethod: IsFireEventMethodFn; + isUserEventMethod: IsUserEventMethodFn; isRenderUtil: IsRenderUtilFn; isRenderVariableDeclarator: IsRenderVariableDeclaratorFn; isDebugUtil: IsDebugUtilFn; @@ -109,6 +112,9 @@ export interface DetectionHelpers { isNodeComingFromTestingLibrary: IsNodeComingFromTestingLibraryFn; } +const USER_EVENT_PACKAGE = '@testing-library/user-event'; +const FIRE_EVENT_NAME = 'fireEvent'; +const USER_EVENT_NAME = 'userEvent'; const RENDER_NAME = 'render'; /** @@ -125,6 +131,7 @@ export function detectTestingLibraryUtils< ): TSESLint.RuleListener => { let importedTestingLibraryNode: ImportModuleNode | null = null; let importedCustomModuleNode: ImportModuleNode | null = null; + let importedUserEventLibraryNode: ImportModuleNode | null = null; // Init options based on shared ESLint settings const customModule = context.settings['testing-library/utils-module']; @@ -174,69 +181,6 @@ export function detectTestingLibraryUtils< return isNodeComingFromTestingLibrary(referenceNodeIdentifier); } - /** - * Determines whether a given node is a simulate event util related to - * Testing Library or not. - * - * In order to determine this, the node must match: - * - indicated simulate event name: fireEvent or userEvent - * - imported from valid Testing Library module (depends on Aggressive - * Reporting) - * - */ - function isTestingLibrarySimulateEventUtil( - node: TSESTree.Identifier, - utilName: 'fireEvent' | 'userEvent' - ): boolean { - const simulateEventUtil = findImportedUtilSpecifier(utilName); - let simulateEventUtilName: string | undefined; - - if (simulateEventUtil) { - simulateEventUtilName = ASTUtils.isIdentifier(simulateEventUtil) - ? simulateEventUtil.name - : simulateEventUtil.local.name; - } else if (isAggressiveModuleReportingEnabled()) { - simulateEventUtilName = utilName; - } - - if (!simulateEventUtilName) { - return false; - } - - const parentMemberExpression: - | TSESTree.MemberExpression - | undefined = isMemberExpression(node.parent) ? node.parent : undefined; - - if (!parentMemberExpression) { - return false; - } - - // make sure that given node it's not fireEvent/userEvent object itself - if ( - [simulateEventUtilName, utilName].includes(node.name) || - (ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === node.name) - ) { - return false; - } - - // check fireEvent.click()/userEvent.click() usage - const regularCall = - ASTUtils.isIdentifier(parentMemberExpression.object) && - parentMemberExpression.object.name === simulateEventUtilName; - - // check testingLibraryUtils.fireEvent.click() or - // testingLibraryUtils.userEvent.click() usage - const wildcardCall = - isMemberExpression(parentMemberExpression.object) && - ASTUtils.isIdentifier(parentMemberExpression.object.object) && - parentMemberExpression.object.object.name === simulateEventUtilName && - ASTUtils.isIdentifier(parentMemberExpression.object.property) && - parentMemberExpression.object.property.name === utilName; - - return regularCall || wildcardCall; - } - /** * Determines whether aggressive module reporting is enabled or not. * @@ -403,7 +347,90 @@ export function detectTestingLibraryUtils< * Determines whether a given node is fireEvent method or not */ const isFireEventMethod: IsFireEventMethodFn = (node) => { - return isTestingLibrarySimulateEventUtil(node, 'fireEvent'); + const fireEventUtil = findImportedUtilSpecifier(FIRE_EVENT_NAME); + let fireEventUtilName: string | undefined; + + if (fireEventUtil) { + fireEventUtilName = ASTUtils.isIdentifier(fireEventUtil) + ? fireEventUtil.name + : fireEventUtil.local.name; + } else if (isAggressiveModuleReportingEnabled()) { + fireEventUtilName = FIRE_EVENT_NAME; + } + + if (!fireEventUtilName) { + return false; + } + + const parentMemberExpression: + | TSESTree.MemberExpression + | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression) { + 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.click() usage + const regularCall = + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.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; + }; + + const isUserEventMethod: IsUserEventMethodFn = (node) => { + const userEvent = findImportedUserEventSpecifier(); + let userEventName: string | undefined; + + if (userEvent) { + userEventName = userEvent.name; + } else if (isAggressiveModuleReportingEnabled()) { + userEventName = USER_EVENT_NAME; + } + + if (!userEventName) { + return false; + } + + const parentMemberExpression: + | TSESTree.MemberExpression + | undefined = isMemberExpression(node.parent) ? node.parent : undefined; + + if (!parentMemberExpression) { + return false; + } + + // make sure that given node it's not userEvent object itself + if ( + [userEventName, USER_EVENT_NAME].includes(node.name) || + (ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === node.name) + ) { + return false; + } + + // check userEvent.click() usage + return ( + ASTUtils.isIdentifier(parentMemberExpression.object) && + parentMemberExpression.object.name === userEventName + ); }; /** @@ -553,6 +580,27 @@ 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 { + const requireNode = importedUserEventLibraryNode.parent as TSESTree.VariableDeclarator; + return requireNode.id as TSESTree.Identifier; + } + + return null; + }; + const getImportedUtilSpecifier = ( node: TSESTree.MemberExpression | TSESTree.Identifier ): TSESTree.ImportClause | TSESTree.Identifier | undefined => { @@ -607,6 +655,7 @@ export function detectTestingLibraryUtils< isFireEventUtil, isUserEventUtil, isFireEventMethod, + isUserEventMethod, isRenderUtil, isRenderVariableDeclarator, isDebugUtil, @@ -644,6 +693,15 @@ export function detectTestingLibraryUtils< ) { importedCustomModuleNode = node; } + + // check only if user-event import not found yet so we avoid + // to override importedUserEventLibraryNode after it's found + if ( + !importedUserEventLibraryNode && + String(node.source.value) === USER_EVENT_PACKAGE + ) { + importedUserEventLibraryNode = node; + } }, // Check if Testing Library related modules are loaded with required. @@ -676,6 +734,18 @@ export function detectTestingLibraryUtils< ) { importedCustomModuleNode = callExpression; } + + if ( + !importedCustomModuleNode && + args.some( + (arg) => + isLiteral(arg) && + typeof arg.value === 'string' && + arg.value === USER_EVENT_PACKAGE + ) + ) { + importedUserEventLibraryNode = callExpression; + } }, }; diff --git a/lib/rules/no-await-sync-events.ts b/lib/rules/no-await-sync-events.ts index 55c92e3c..07081c46 100644 --- a/lib/rules/no-await-sync-events.ts +++ b/lib/rules/no-await-sync-events.ts @@ -1,16 +1,20 @@ +import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; import { - ASTUtils, - ESLintUtils, - TSESTree, -} from '@typescript-eslint/experimental-utils'; -import { getDocsUrl, SYNC_EVENTS } from '../utils'; -import { isObjectExpression, isProperty } from '../node-utils'; + getDeepestIdentifierNode, + getPropertyIdentifierNode, + isLiteral, + isObjectExpression, + isProperty, +} from '../node-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; + export const RULE_NAME = 'no-await-sync-events'; export type MessageIds = 'noAwaitSyncEvents'; type Options = []; -const SYNC_EVENTS_REGEXP = new RegExp(`^(${SYNC_EVENTS.join('|')})$`); -export default ESLintUtils.RuleCreator(getDocsUrl)({ +const USER_EVENT_ASYNC_EXCEPTIONS: string[] = ['type', 'keyboard']; + +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'problem', @@ -20,51 +24,63 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ recommended: 'error', }, messages: { - noAwaitSyncEvents: '`{{ name }}` does not need `await` operator', + noAwaitSyncEvents: + '`{{ name }}` is sync and does not need `await` operator', }, fixable: null, schema: [], }, defaultOptions: [], - create(context) { - // userEvent.type() is an exception, which returns a - // Promise. But it is only necessary to wait when delay - // option is specified. So this rule has a special exception - // for the case await userEvent.type(element, 'abc', {delay: 1234}) + create(context, _, helpers) { + // userEvent.type() and userEvent.keyboard() are exceptions, which returns a + // Promise. But it is only necessary to wait when delay option other than 0 + // is specified. So this rule has a special exception for the case await: + // - userEvent.type(element, 'abc', {delay: 1234}) + // - userEvent.keyboard('abc', {delay: 1234}) return { - [`AwaitExpression > CallExpression > MemberExpression > Identifier[name=${SYNC_EVENTS_REGEXP}]`]( - node: TSESTree.Identifier - ) { - const memberExpression = node.parent as TSESTree.MemberExpression; - const methodNode = memberExpression.property as TSESTree.Identifier; - const callExpression = memberExpression.parent as TSESTree.CallExpression; - const lastArg = - callExpression.arguments[callExpression.arguments.length - 1]; - const withDelay = + 'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) { + const simulateEventFunctionIdentifier = getDeepestIdentifierNode(node); + + const isSimulateEventMethod = + helpers.isUserEventMethod(simulateEventFunctionIdentifier) || + helpers.isFireEventMethod(simulateEventFunctionIdentifier); + + if (!isSimulateEventMethod) { + return; + } + + const lastArg = node.arguments[node.arguments.length - 1]; + + const hasDelay = isObjectExpression(lastArg) && lastArg.properties.some( (property) => isProperty(property) && ASTUtils.isIdentifier(property.key) && - property.key.name === 'delay' + property.key.name === 'delay' && + isLiteral(property.value) && + property.value.value > 0 ); + const simulateEventFunctionName = simulateEventFunctionIdentifier.name; + if ( - !( - node.name === 'userEvent' && - ['type', 'keyboard'].includes(methodNode.name) && - withDelay - ) + USER_EVENT_ASYNC_EXCEPTIONS.includes(simulateEventFunctionName) && + hasDelay ) { - context.report({ - node: methodNode, - messageId: 'noAwaitSyncEvents', - data: { - name: `${node.name}.${methodNode.name}`, - }, - }); + return; } + + context.report({ + node, + messageId: 'noAwaitSyncEvents', + data: { + name: `${ + getPropertyIdentifierNode(node).name + }.${simulateEventFunctionName}`, + }, + }); }, }; }, diff --git a/lib/utils.ts b/lib/utils.ts index 1e4f0064..71850468 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -63,7 +63,7 @@ const ASYNC_UTILS = [ 'waitForDomChange', ] as const; -const SYNC_EVENTS = ['fireEvent', 'userEvent']; +const EVENTS_SIMULATORS = ['fireEvent', 'userEvent'] as const; const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll']; @@ -116,7 +116,7 @@ export { ASYNC_QUERIES_COMBINATIONS, ALL_QUERIES_COMBINATIONS, ASYNC_UTILS, - SYNC_EVENTS, + EVENTS_SIMULATORS, TESTING_FRAMEWORK_SETUP_HOOKS, LIBRARY_MODULES, PROPERTIES_RETURNING_NODES, diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 3fba6546..39158e9c 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -71,6 +71,36 @@ ruleTester.run(RULE_NAME, rule, { `, }, + // Test Cases for user-event imports + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from 'somewhere-else' + userEvent.click(element) + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import '@testing-library/user-event' + userEvent.click() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { click } from '@testing-library/user-event' + userEvent.click() + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import * as incorrect from '@testing-library/user-event' + userEvent.click() + `, + }, + // Test Cases for renders { code: ` @@ -430,6 +460,54 @@ ruleTester.run(RULE_NAME, rule, { errors: [{ line: 3, column: 7, messageId: 'fakeError' }], }, + // Test Cases for user-event imports + { + code: ` + import userEvent from 'somewhere-else' + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import userEvent from '@testing-library/user-event' + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import renamed from '@testing-library/user-event' + renamed.click(element) + `, + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, + { + code: ` + const userEvent = require('somewhere-else') + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + const userEvent = require('@testing-library/user-event') + userEvent.click(element) + `, + errors: [{ line: 3, column: 17, messageId: 'userEventError' }], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + const renamed = require('@testing-library/user-event') + renamed.click(element) + `, + errors: [{ line: 3, column: 15, messageId: 'userEventError' }], + }, + // Test Cases for renders { code: ` diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index ece0ccbd..38513a4e 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -15,6 +15,7 @@ type MessageIds = | 'queryByError' | 'findByError' | 'customQueryError' + | 'userEventError' | 'presenceAssertError' | 'absenceAssertError'; @@ -36,6 +37,7 @@ export default createTestingLibraryRule({ queryByError: 'some error related to queryBy reported', findByError: 'some error related to findBy reported', customQueryError: 'some error related to a customQuery reported', + userEventError: 'some error related to userEvent reported', presenceAssertError: 'some error related to presence assert reported', absenceAssertError: 'some error related to absence assert reported', }, @@ -59,6 +61,10 @@ export default createTestingLibraryRule({ }); } + if (helpers.isUserEventMethod(node)) { + return context.report({ node, messageId: 'userEventError' }); + } + // force queries to be reported if (helpers.isCustomQuery(node)) { return context.report({ node, messageId: 'customQueryError' }); @@ -90,7 +96,6 @@ export default createTestingLibraryRule({ const reportImportDeclaration = (node: TSESTree.ImportDeclaration) => { // This is just to check that defining an `ImportDeclaration` doesn't // override `ImportDeclaration` from `detectTestingLibraryUtils` - if (node.source.value === 'report-me') { context.report({ node, messageId: 'fakeError' }); } diff --git a/tests/lib/rules/no-await-sync-events.test.ts b/tests/lib/rules/no-await-sync-events.test.ts index 744fafc1..5289b9b4 100644 --- a/tests/lib/rules/no-await-sync-events.test.ts +++ b/tests/lib/rules/no-await-sync-events.test.ts @@ -1,10 +1,9 @@ import { createRuleTester } from '../test-utils'; import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-events'; -import { SYNC_EVENTS } from '../../../lib/utils'; const ruleTester = createRuleTester(); -const fireEventFunctions = [ +const FIRE_EVENT_FUNCTIONS = [ 'copy', 'cut', 'paste', @@ -89,7 +88,7 @@ const fireEventFunctions = [ 'gotPointerCapture', 'lostPointerCapture', ]; -const userEventFunctions = [ +const USER_EVENT_SYNC_FUNCTIONS = [ 'clear', 'click', 'dblClick', @@ -97,48 +96,44 @@ const userEventFunctions = [ 'deselectOptions', 'upload', // 'type', + // 'keyboard', 'tab', 'paste', 'hover', 'unhover', ]; -let eventFunctions: string[] = []; -SYNC_EVENTS.forEach((event) => { - switch (event) { - case 'fireEvent': - eventFunctions = eventFunctions.concat( - fireEventFunctions.map((f: string): string => `${event}.${f}`) - ); - break; - case 'userEvent': - eventFunctions = eventFunctions.concat( - userEventFunctions.map((f: string): string => `${event}.${f}`) - ); - break; - default: - eventFunctions.push(`${event}.anyFunc`); - } -}); ruleTester.run(RULE_NAME, rule, { valid: [ - // sync events without await are valid - // userEvent.type() is an exception - ...eventFunctions.map((func) => ({ + // sync fireEvents methods without await are valid + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ + code: `() => { + fireEvent.${func}('foo') + } + `, + })), + // sync userEvent methods without await are valid + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ code: `() => { - ${func}('foo') + userEvent.${func}('foo') } `, })), { code: `() => { - userEvent.type('foo') + userEvent.type(element, 'foo') } `, }, { code: `() => { - await userEvent.type('foo', 'bar', {delay: 1234}) + userEvent.keyboard('foo') + } + `, + }, + { + code: `() => { + await userEvent.type(element, 'bar', {delay: 1234}) } `, }, @@ -148,31 +143,143 @@ ruleTester.run(RULE_NAME, rule, { } `, }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { fireEvent } from 'somewhere-else'; + test('should not report fireEvent.click() not related to Testing Library', async() => { + await fireEvent.click('foo'); + }); + `, + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { fireEvent as renamedFireEvent } from 'somewhere-else'; + import renamedUserEvent from '@testing-library/user-event'; + import { fireEvent, userEvent } from 'somewhere-else' + + test('should not report unused renamed methods', async() => { + await fireEvent.click('foo'); + await userEvent.type('foo', 'bar', { delay: 5 }); + await userEvent.keyboard('foo', { delay: 5 }); + }); + `, + }, ], invalid: [ - // sync events with await operator are not valid - ...eventFunctions.map((func) => ({ + // sync fireEvent methods with await operator are not valid + ...FIRE_EVENT_FUNCTIONS.map((func) => ({ code: ` import { fireEvent } from '@testing-library/framework'; + test('should report fireEvent.${func} sync event awaited', async() => { + await fireEvent.${func}('foo'); + }); + `, + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `fireEvent.${func}` }, + }, + ], + })), + // sync userEvent sync methods with await operator are not valid + ...USER_EVENT_SYNC_FUNCTIONS.map((func) => ({ + code: ` import userEvent from '@testing-library/user-event'; - test('should report sync event awaited', async() => { - await ${func}('foo'); + test('should report userEvent.${func} sync event awaited', async() => { + await userEvent.${func}('foo'); }); `, - errors: [{ line: 5, messageId: 'noAwaitSyncEvents' }], + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: `userEvent.${func}` }, + }, + ], })), { code: ` import userEvent from '@testing-library/user-event'; - test('should report sync event awaited', async() => { - await userEvent.type('foo', 'bar', {hello: 1234}); - await userEvent.keyboard('foo', {hello: 1234}); + test('should report async events without delay awaited', async() => { + await userEvent.type('foo', 'bar'); + await userEvent.keyboard('foo'); + }); + `, + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, + ], + }, + { + code: ` + import userEvent from '@testing-library/user-event'; + test('should report async events with 0 delay awaited', async() => { + await userEvent.type('foo', 'bar', { delay: 0 }); + await userEvent.keyboard('foo', { delay: 0 }); + }); + `, + errors: [ + { + line: 4, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.type' }, + }, + { + line: 5, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'userEvent.keyboard' }, + }, + ], + }, + { + settings: { 'testing-library/utils-module': 'test-utils' }, + code: ` + import { fireEvent as renamedFireEvent } from 'test-utils'; + import renamedUserEvent from '@testing-library/user-event'; + + test('should report renamed invalid cases with Aggressive Reporting disabled', async() => { + await renamedFireEvent.click('foo'); + await renamedUserEvent.type('foo', 'bar', { delay: 0 }); + await renamedUserEvent.keyboard('foo', { delay: 0 }); }); `, errors: [ - { line: 4, messageId: 'noAwaitSyncEvents' }, - { line: 5, messageId: 'noAwaitSyncEvents' }, + { + line: 6, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedFireEvent.click' }, + }, + { + line: 7, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedUserEvent.type' }, + }, + { + line: 8, + column: 17, + messageId: 'noAwaitSyncEvents', + data: { name: 'renamedUserEvent.keyboard' }, + }, ], }, ],