Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ To enable this configuration use the `extends` property in your
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
Expand Down
73 changes: 73 additions & 0 deletions docs/rules/no-side-effects-wait-for.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Side effects inside `waitFor` are not preferred (no-side-effects-wait-for)

## Rule Details

This rule aims to avoid the usage of side effects actions (`fireEvent` or `useEvent`) inside `waitFor`.
Since `waitFor` is intended for things that have a non-deterministic amount of time between the action you performed and the assertion passing,
the callback can be called (or checked for errors) a non-deterministic number of times and frequency.
This will make your side-effect run multiple times.

Example of **incorrect** code for this rule:

```js
const foo = async () => {
await waitFor(() => {
fireEvent.keyDown(input, { key: 'ArrowDown' });
expect(b).toEqual('b');
});

// or
await waitFor(function() {
fireEvent.keyDown(input, { key: 'ArrowDown' });
expect(b).toEqual('b');
});

// or
await waitFor(() => {
userEvent.keyDown(input, { key: 'ArrowDown' });
expect(b).toEqual('b');
});

// or
await waitFor(function() {
userEvent.keyDown(input, { key: 'ArrowDown' });
expect(b).toEqual('b');
});
};
```

Examples of **correct** code for this rule:

```js
const foo = async () => {
fireEvent.keyDown(input, { key: 'ArrowDown' });
await waitFor(() => {
expect(b).toEqual('b');
});

// or
fireEvent.keyDown(input, { key: 'ArrowDown' });
await waitFor(function() {
expect(b).toEqual('b');
});

// or
userEvent.keyDown(input, { key: 'ArrowDown' });
await waitFor(() => {
expect(b).toEqual('b');
});

// or
userEvent.keyDown(input, { key: 'ArrowDown' });
await waitFor(function() {
expect(b).toEqual('b');
});
};
```

## Further Reading

- [about `waitFor`](https://testing-library.com/docs/dom-testing-library/api-async#waitfor)
- [about `userEvent`](https://github.com/testing-library/user-event)
- [about `fireEvent`](https://testing-library.com/docs/dom-testing-library/api-events)
- [inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#performing-side-effects-in-waitfor)
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import preferScreenQueries from './rules/prefer-screen-queries';
import preferWaitFor from './rules/prefer-wait-for';
import noMultipleAssertionsWaitFor from './rules/no-multiple-assertions-wait-for'
import preferFindBy from './rules/prefer-find-by';
import noSideEffectsWaitFor from './rules/no-side-effects-wait-for'

const rules = {
'await-async-query': awaitAsyncQuery,
Expand All @@ -28,6 +29,7 @@ const rules = {
'no-manual-cleanup': noManualCleanup,
'no-multiple-assertions-wait-for': noMultipleAssertionsWaitFor,
'no-promise-in-fire-event': noPromiseInFireEvent,
'no-side-effects-wait-for': noSideEffectsWaitFor,
'no-wait-for-empty-callback': noWaitForEmptyCallback,
'prefer-explicit-assert': preferExplicitAssert,
'prefer-find-by': preferFindBy,
Expand Down
65 changes: 65 additions & 0 deletions lib/rules/no-side-effects-wait-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'
import { getDocsUrl } from '../utils'
import { isBlockStatement, findClosestCallNode, isMemberExpression, isCallExpression, isIdentifier } from '../node-utils'

export const RULE_NAME: string = 'no-side-effects-wait-for';

const WAIT_EXPRESSION_QUERY: string =
'CallExpression[callee.name=/^(waitFor)$/]';

const SIDE_EFFECTS: Array<string> = ['fireEvent', 'userEvent']

export type MessageIds = 'noSideEffectsWaitFor';
type Options = [];

export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description:
"It's preferred to avoid side effects in `waitFor`",
category: 'Best Practices',
recommended: false,
},
messages: {
noSideEffectsWaitFor: 'Avoid using side effects within `waitFor` callback',
},
fixable: null,
schema: [],
},
defaultOptions: [],
create: function(context) {
function reportSideEffects(
node: TSESTree.BlockStatement
) {
const totalSideEffects = (body: Array<TSESTree.Node>): Array<TSESTree.Node> =>
body.filter((node: TSESTree.ExpressionStatement) => {
if (
isCallExpression(node.expression) &&
isMemberExpression(node.expression.callee) &&
isIdentifier(node.expression.callee.object)
) {
const object: TSESTree.Identifier = node.expression.callee.object
const identifierName: string = object.name
return SIDE_EFFECTS.includes(identifierName)
} else {
return false
}
})

if (isBlockStatement(node) && totalSideEffects(node.body).length > 0) {
context.report({
node,
loc: node.loc.start,
messageId: 'noSideEffectsWaitFor',
});
}
}

return {
[`${WAIT_EXPRESSION_QUERY} > ArrowFunctionExpression > BlockStatement`]: reportSideEffects,
[`${WAIT_EXPRESSION_QUERY} > FunctionExpression > BlockStatement`]: reportSideEffects,
};
}
})
188 changes: 188 additions & 0 deletions tests/lib/rules/no-side-effects-wait-for.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { createRuleTester } from '../test-utils';
import rule, { RULE_NAME } from '../../../lib/rules/no-side-effects-wait-for';

const ruleTester = createRuleTester({
ecmaFeatures: {
jsx: true,
},
});

ruleTester.run(RULE_NAME, rule, {
valid: [
{
code: `
await waitFor(() => expect(a).toEqual('a'))
`,
},
{
code: `
await waitFor(function() {
expect(a).toEqual('a')
})
`,
},
{
code: `
await waitFor(() => {
console.log('testing-library')
expect(b).toEqual('b')
})
`,
},
{
code: `
await waitFor(function() {
console.log('testing-library')
expect(b).toEqual('b')
})
`,
},
{
code: `
await waitFor(() => {})
`,
},
{
code: `
await waitFor(function() {})
`,
},
{
code: `
await waitFor(() => {
// testing
})
`,
},
{
code: `
await waitFor(function() {
// testing
})
`,
},
{
code: `
fireEvent.keyDown(input, {key: 'ArrowDown'})
await waitFor(() => {
expect(b).toEqual('b')
})
`
}, {
code: `
fireEvent.keyDown(input, {key: 'ArrowDown'})
await waitFor(function() {
expect(b).toEqual('b')
})
`
}
],
invalid: [
// fireEvent
{
code: `
await waitFor(() => {
fireEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(() => {
expect(b).toEqual('b')
fireEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(() => {
fireEvent.keyDown(input, {key: 'ArrowDown'})
expect(b).toEqual('b')
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(function() {
fireEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(function() {
expect(b).toEqual('b')
fireEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(function() {
fireEvent.keyDown(input, {key: 'ArrowDown'})
expect(b).toEqual('b')
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
// userEvent
{
code: `
await waitFor(() => {
userEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(() => {
expect(b).toEqual('b')
userEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(() => {
userEvent.keyDown(input, {key: 'ArrowDown'})
expect(b).toEqual('b')
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(function() {
userEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(function() {
expect(b).toEqual('b')
userEvent.keyDown(input, {key: 'ArrowDown'})
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
},
{
code: `
await waitFor(function() {
userEvent.keyDown(input, {key: 'ArrowDown'})
expect(b).toEqual('b')
})
`,
errors: [{ messageId: 'noSideEffectsWaitFor' }]
}
]
})