-
Notifications
You must be signed in to change notification settings - Fork 153
feat(rule): add no-await-sync-events rule #240
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
Changes from 1 commit
8017327
ce84207
780ac25
295ba37
47759a6
d3c3897
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Disallow unnecessary `await` for sync events (no-await-sync-events) | ||
|
|
||
| Ensure that sync 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`. Some examples are: | ||
|
|
||
| - `fireEvent.click` | ||
| - `fireEvent.select` | ||
| - `userEvent.tab` | ||
| - `userEvent.hover` | ||
|
|
||
| This rule aims to prevent users from waiting for those function calls. | ||
|
|
||
| Examples of **incorrect** code for this rule: | ||
|
|
||
| ```js | ||
| const foo = async () => { | ||
| // ... | ||
| await fireEvent.click(button); | ||
| // ... | ||
| }; | ||
|
|
||
| const bar = () => { | ||
| // ... | ||
| await userEvent.tab(); | ||
| // ... | ||
| }; | ||
| ``` | ||
|
|
||
| Examples of **correct** code for this rule: | ||
|
|
||
| ```js | ||
| const foo = () => { | ||
| // ... | ||
| fireEvent.click(button); | ||
| // ... | ||
| }; | ||
|
|
||
| const bar = () => { | ||
| // ... | ||
| userEvent.tab(); | ||
| // ... | ||
| }; | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| There is another rule `await-fire-event`, which is only in Vue Testing | ||
| Library. Please do not confuse with this rule. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
| import { getDocsUrl, ASYNC_EVENTS } from '../utils'; | ||
|
|
||
| export const RULE_NAME = 'no-await-sync-events'; | ||
| export type MessageIds = 'noAwaitSyncEvents'; | ||
| type Options = []; | ||
|
|
||
| const ASYNC_EVENTS_REGEXP = new RegExp(`^(${ASYNC_EVENTS.join('|')})$`); | ||
| export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({ | ||
| name: RULE_NAME, | ||
| meta: { | ||
| type: 'problem', | ||
| docs: { | ||
| description: 'Disallow unnecessary `await` for sync events', | ||
| category: 'Best Practices', | ||
| recommended: 'error', | ||
| }, | ||
| messages: { | ||
| noAwaitSyncEvents: '`{{ name }}` does not need `await` operator', | ||
| }, | ||
| fixable: null, | ||
| schema: [], | ||
| }, | ||
| defaultOptions: [], | ||
|
|
||
| create(context) { | ||
| // userEvent.type() is an exception, which returns a | ||
| // Promise, even it resolves immediately. | ||
| // for the sake of semantically correct, w/ or w/o await | ||
| // are both OK | ||
| return { | ||
| [`AwaitExpression > CallExpression > MemberExpression > Identifier[name=${ASYNC_EVENTS_REGEXP}]`]( | ||
| node: TSESTree.Identifier | ||
| ) { | ||
| const memberExpression = node.parent as TSESTree.MemberExpression; | ||
| const methodNode = memberExpression.property as TSESTree.Identifier; | ||
|
|
||
| if (!(node.name === 'userEvent' && methodNode.name === 'type')) { | ||
| context.report({ | ||
| node: methodNode, | ||
| messageId: 'noAwaitSyncEvents', | ||
| data: { | ||
| name: `${node.name}.${methodNode.name}`, | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,6 +63,11 @@ const ASYNC_UTILS = [ | |
| 'waitForDomChange', | ||
| ]; | ||
|
|
||
| const ASYNC_EVENTS = [ | ||
|
||
| 'fireEvent', | ||
| 'userEvent', | ||
| ]; | ||
|
|
||
| const TESTING_FRAMEWORK_SETUP_HOOKS = ['beforeEach', 'beforeAll']; | ||
|
|
||
| const PRESENCE_MATCHERS = ['toBeInTheDocument', 'toBeTruthy', 'toBeDefined']; | ||
|
|
@@ -78,6 +83,7 @@ export { | |
| ASYNC_QUERIES_COMBINATIONS, | ||
| ALL_QUERIES_COMBINATIONS, | ||
| ASYNC_UTILS, | ||
| ASYNC_EVENTS, | ||
| TESTING_FRAMEWORK_SETUP_HOOKS, | ||
| LIBRARY_MODULES, | ||
| PRESENCE_MATCHERS, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import { createRuleTester } from '../test-utils'; | ||
| import rule, { RULE_NAME } from '../../../lib/rules/no-await-sync-events'; | ||
| import { ASYNC_EVENTS } from '../../../lib/utils'; | ||
|
|
||
| const ruleTester = createRuleTester(); | ||
|
|
||
| const fireEventFunctions = [ | ||
| 'copy', | ||
| 'cut', | ||
| 'paste', | ||
| 'compositionEnd', | ||
| 'compositionStart', | ||
| 'compositionUpdate', | ||
| 'keyDown', | ||
| 'keyPress', | ||
| 'keyUp', | ||
| 'focus', | ||
| 'blur', | ||
| 'focusIn', | ||
| 'focusOut', | ||
| 'change', | ||
| 'input', | ||
| 'invalid', | ||
| 'submit', | ||
| 'reset', | ||
| 'click', | ||
| 'contextMenu', | ||
| 'dblClick', | ||
| 'drag', | ||
| 'dragEnd', | ||
| 'dragEnter', | ||
| 'dragExit', | ||
| 'dragLeave', | ||
| 'dragOver', | ||
| 'dragStart', | ||
| 'drop', | ||
| 'mouseDown', | ||
| 'mouseEnter', | ||
| 'mouseLeave', | ||
| 'mouseMove', | ||
| 'mouseOut', | ||
| 'mouseOver', | ||
| 'mouseUp', | ||
| 'popState', | ||
| 'select', | ||
| 'touchCancel', | ||
| 'touchEnd', | ||
| 'touchMove', | ||
| 'touchStart', | ||
| 'scroll', | ||
| 'wheel', | ||
| 'abort', | ||
| 'canPlay', | ||
| 'canPlayThrough', | ||
| 'durationChange', | ||
| 'emptied', | ||
| 'encrypted', | ||
| 'ended', | ||
| 'loadedData', | ||
| 'loadedMetadata', | ||
| 'loadStart', | ||
| 'pause', | ||
| 'play', | ||
| 'playing', | ||
| 'progress', | ||
| 'rateChange', | ||
| 'seeked', | ||
| 'seeking', | ||
| 'stalled', | ||
| 'suspend', | ||
| 'timeUpdate', | ||
| 'volumeChange', | ||
| 'waiting', | ||
| 'load', | ||
| 'error', | ||
| 'animationStart', | ||
| 'animationEnd', | ||
| 'animationIteration', | ||
| 'transitionEnd', | ||
| 'doubleClick', | ||
| 'pointerOver', | ||
| 'pointerEnter', | ||
| 'pointerDown', | ||
| 'pointerMove', | ||
| 'pointerUp', | ||
| 'pointerCancel', | ||
| 'pointerOut', | ||
| 'pointerLeave', | ||
| 'gotPointerCapture', | ||
| 'lostPointerCapture', | ||
| ]; | ||
| const userEventFunctions = [ | ||
| 'clear', | ||
| 'click', | ||
| 'dblClick', | ||
| 'selectOptions', | ||
| 'deselectOptions', | ||
| 'upload', | ||
| // 'type', | ||
| 'tab', | ||
| 'paste', | ||
| 'hover', | ||
| 'unhover', | ||
| ]; | ||
| let eventFunctions: string[] = []; | ||
| ASYNC_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: [ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to include more tests for checking There is an ongoing refactor for v4 of the plugin which actually will check that for all rules out of the box, so I don't know if you prefer to wait for that version to be released. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Belco90 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The former, so we don't report const declared by the user with the same name. As mentioned, the internal refactor for v4 will pass some helpers to all rules to check this generically, so I'm not sure if you prefer to wait for that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the clarification. |
||
| // sync events without await are valid | ||
| // userEvent.type() is an exception | ||
| ...eventFunctions.map(func => ({ | ||
| code: `() => { | ||
| ${func}('foo') | ||
| } | ||
| `, | ||
| })), | ||
| { | ||
| code: `() => { | ||
| userEvent.type('foo') | ||
| } | ||
| `, | ||
| }, | ||
| { | ||
| code: `() => { | ||
| await userEvent.type('foo') | ||
| } | ||
| `, | ||
| }, | ||
| ], | ||
|
|
||
| invalid: [ | ||
| // sync events with await operator are not valid | ||
| ...eventFunctions.map(func => ({ | ||
| code: `() => { | ||
|
||
| await ${func}('foo') | ||
| } | ||
| `, | ||
| errors: [ | ||
| { | ||
| messageId: 'noAwaitSyncEvents', | ||
| }, | ||
| ], | ||
| })), | ||
| ], | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is technically true, but you don't need to always wait for it. From user-event doc:
So should this rule report about
userEvent.typeif no delay option passed? I think soThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with that ☝️ we should only await if the delay option is used. It would be great if we can cover that scenario with the rule
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. upgraded
the rule is, only if userEvent.type with delay option, await is valid. For all the other cases, it disallows await.