-
Notifications
You must be signed in to change notification settings - Fork 141
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
feat(rule): add no-await-sync-events rule #240
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 = [ | ||
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. These are SYNC_EVENTS actually, right? 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. I wouldn't say there are events either - there are helpers or utilities.. Events would be "tab", "input", "change", "click" and so on... 🤔 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. initially thought it should be no-await-sync-utils, but feel not so correct...I can rename it to utils if it makes more sense. |
||
'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: `() => { | ||
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. Can you write more realistic code here? So you end up with something like: import { fireEvent } from '@testing-library/framework'
import userEvent from '@testing-library/user-event'
import MyComponent from './MyComponent'
it('should report sync event awaited', async () => {
render(<MyComponent />)
await ${func}('foo')
await findByQuery('some element')
}) and then check in the errors which line the error should appear. 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. updated |
||
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.type
if 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
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.