diff --git a/docs/rules/no-wait-for-side-effects.md b/docs/rules/no-wait-for-side-effects.md
index 6f81179d..b545fae5 100644
--- a/docs/rules/no-wait-for-side-effects.md
+++ b/docs/rules/no-wait-for-side-effects.md
@@ -2,7 +2,7 @@
## Rule Details
-This rule aims to avoid the usage of side effects actions (`fireEvent` or `userEvent`) inside `waitFor`.
+This rule aims to avoid the usage of side effects actions (`fireEvent`, `userEvent` or `render`) 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.
@@ -32,6 +32,18 @@ Example of **incorrect** code for this rule:
userEvent.click(button);
expect(b).toEqual('b');
});
+
+ // or
+ await waitFor(() => {
+ render()
+ expect(b).toEqual('b');
+ });
+
+ // or
+ await waitFor(function() {
+ render()
+ expect(b).toEqual('b');
+ });
};
```
@@ -60,6 +72,18 @@ Examples of **correct** code for this rule:
await waitFor(function() {
expect(b).toEqual('b');
});
+
+ // or
+ render()
+ await waitFor(() => {
+ expect(b).toEqual('b');
+ });
+
+ // or
+ render()
+ await waitFor(function() {
+ expect(b).toEqual('b');
+ });
};
```
diff --git a/lib/node-utils/is-node-of-type.ts b/lib/node-utils/is-node-of-type.ts
index 89d8880b..ac8dbb5f 100644
--- a/lib/node-utils/is-node-of-type.ts
+++ b/lib/node-utils/is-node-of-type.ts
@@ -16,6 +16,15 @@ export const isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression);
export const isExpressionStatement = isNodeOfType(
AST_NODE_TYPES.ExpressionStatement
);
+export const isVariableDeclaration = isNodeOfType(
+ AST_NODE_TYPES.VariableDeclaration
+);
+export const isAssignmentExpression = isNodeOfType(
+ AST_NODE_TYPES.AssignmentExpression
+);
+export const isSequenceExpression = isNodeOfType(
+ AST_NODE_TYPES.SequenceExpression
+);
export const isImportDeclaration = isNodeOfType(
AST_NODE_TYPES.ImportDeclaration
);
diff --git a/lib/rules/no-wait-for-side-effects.ts b/lib/rules/no-wait-for-side-effects.ts
index c48735ed..b9a9cdd5 100644
--- a/lib/rules/no-wait-for-side-effects.ts
+++ b/lib/rules/no-wait-for-side-effects.ts
@@ -2,6 +2,10 @@ import { TSESTree } from '@typescript-eslint/experimental-utils';
import {
getPropertyIdentifierNode,
isExpressionStatement,
+ isVariableDeclaration,
+ isAssignmentExpression,
+ isCallExpression,
+ isSequenceExpression,
} from '../node-utils';
import { createTestingLibraryRule } from '../create-testing-library-rule';
@@ -32,7 +36,11 @@ export default createTestingLibraryRule({
defaultOptions: [],
create: function (context, _, helpers) {
function isCallerWaitFor(
- node: TSESTree.BlockStatement | TSESTree.CallExpression
+ node:
+ | TSESTree.BlockStatement
+ | TSESTree.CallExpression
+ | TSESTree.AssignmentExpression
+ | TSESTree.SequenceExpression
): boolean {
if (!node.parent) {
return false;
@@ -48,22 +56,78 @@ export default createTestingLibraryRule({
);
}
+ function isRenderInVariableDeclaration(node: TSESTree.Node) {
+ return (
+ isVariableDeclaration(node) &&
+ node.declarations.some(helpers.isRenderVariableDeclarator)
+ );
+ }
+
+ function isRenderInExpressionStatement(node: TSESTree.Node) {
+ if (
+ !isExpressionStatement(node) ||
+ !isAssignmentExpression(node.expression)
+ ) {
+ return false;
+ }
+
+ const expressionIdentifier = getPropertyIdentifierNode(
+ node.expression.right
+ );
+
+ if (!expressionIdentifier) {
+ return false;
+ }
+
+ return helpers.isRenderUtil(expressionIdentifier);
+ }
+
+ function isRenderInAssignmentExpression(node: TSESTree.Node) {
+ if (!isAssignmentExpression(node)) {
+ return false;
+ }
+
+ const expressionIdentifier = getPropertyIdentifierNode(node.right);
+ if (!expressionIdentifier) {
+ return false;
+ }
+
+ return helpers.isRenderUtil(expressionIdentifier);
+ }
+
+ function isRenderInSequenceAssignment(node: TSESTree.Node) {
+ if (!isSequenceExpression(node)) {
+ return false;
+ }
+
+ return node.expressions.some(isRenderInAssignmentExpression);
+ }
+
function getSideEffectNodes(
body: TSESTree.Node[]
): TSESTree.ExpressionStatement[] {
return body.filter((node) => {
- if (!isExpressionStatement(node)) {
+ if (!isExpressionStatement(node) && !isVariableDeclaration(node)) {
return false;
}
+ if (
+ isRenderInVariableDeclaration(node) ||
+ isRenderInExpressionStatement(node)
+ ) {
+ return true;
+ }
+
const expressionIdentifier = getPropertyIdentifierNode(node);
+
if (!expressionIdentifier) {
return false;
}
return (
helpers.isFireEventUtil(expressionIdentifier) ||
- helpers.isUserEventUtil(expressionIdentifier)
+ helpers.isUserEventUtil(expressionIdentifier) ||
+ helpers.isRenderUtil(expressionIdentifier)
);
}) as TSESTree.ExpressionStatement[];
}
@@ -86,19 +150,33 @@ export default createTestingLibraryRule({
}
}
- function reportImplicitReturnSideEffect(node: TSESTree.CallExpression) {
+ function reportImplicitReturnSideEffect(
+ node:
+ | TSESTree.CallExpression
+ | TSESTree.AssignmentExpression
+ | TSESTree.SequenceExpression
+ ) {
if (!isCallerWaitFor(node)) {
return;
}
- const expressionIdentifier = getPropertyIdentifierNode(node.callee);
- if (!expressionIdentifier) {
+ const expressionIdentifier = isCallExpression(node)
+ ? getPropertyIdentifierNode(node.callee)
+ : null;
+
+ if (
+ !expressionIdentifier &&
+ !isRenderInAssignmentExpression(node) &&
+ !isRenderInSequenceAssignment(node)
+ ) {
return;
}
if (
+ expressionIdentifier &&
!helpers.isFireEventUtil(expressionIdentifier) &&
- !helpers.isUserEventUtil(expressionIdentifier)
+ !helpers.isUserEventUtil(expressionIdentifier) &&
+ !helpers.isRenderUtil(expressionIdentifier)
) {
return;
}
@@ -112,6 +190,8 @@ export default createTestingLibraryRule({
return {
'CallExpression > ArrowFunctionExpression > BlockStatement': reportSideEffects,
'CallExpression > ArrowFunctionExpression > CallExpression': reportImplicitReturnSideEffect,
+ 'CallExpression > ArrowFunctionExpression > AssignmentExpression': reportImplicitReturnSideEffect,
+ 'CallExpression > ArrowFunctionExpression > SequenceExpression': reportImplicitReturnSideEffect,
'CallExpression > FunctionExpression > BlockStatement': reportSideEffects,
};
},
diff --git a/tests/lib/rules/no-wait-for-side-effects.test.ts b/tests/lib/rules/no-wait-for-side-effects.test.ts
index 8d1b552d..f802ffe5 100644
--- a/tests/lib/rules/no-wait-for-side-effects.test.ts
+++ b/tests/lib/rules/no-wait-for-side-effects.test.ts
@@ -180,8 +180,340 @@ ruleTester.run(RULE_NAME, rule, {
await waitFor(() => userEvent.click(button))
`,
},
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'somewhere-else';
+ await waitFor(() => render())
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'somewhere-else';
+ await waitFor(() => {
+ const { container } = render()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'somewhere-else';
+ const { rerender } = render()
+ await waitFor(() => {
+ rerender()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': '~/test-utils' },
+ code: `
+ import { waitFor } from '~/test-utils';
+ import { render } from 'somewhere-else';
+ await waitFor(() => render())
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': '~/test-utils' },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { render } from 'somewhere-else';
+ await waitFor(() => render())
+ `,
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderWrapper } from 'somewhere-else';
+ await waitFor(() => renderWrapper())
+ `,
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderWrapper } from 'somewhere-else';
+ await waitFor(() => {
+ renderWrapper()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderWrapper } from 'somewhere-else';
+ await waitFor(() => {
+ const { container } = renderWrapper()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'somewhere-else';
+ await waitFor(() => {
+ render()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'test-utils';
+ import { render } from 'somewhere-else';
+ await waitFor(() => {
+ render()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderWrapper } from 'somewhere-else';
+ await waitFor(() => {
+ renderWrapper()
+ })
+ `,
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => result = renderWrapper())
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'test-utils';
+ import { render } from 'somewhere-else';
+ await waitFor(() => result = render())
+ `,
+ },
+ {
+ settings: { 'testing-library/utils-module': 'test-utils' },
+ code: `
+ import { waitFor } from 'somewhere-else';
+ await waitFor(() => result = render())
+ `,
+ },
],
invalid: [
+ // render
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => render())
+ `,
+ errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(function() {
+ render()
+ })
+ `,
+ errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(function() {
+ const { container } = renderHelper()
+ })
+ `,
+ errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderHelper } from 'somewhere-else';
+ await waitFor(() => renderHelper())
+ `,
+ errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderHelper } from 'somewhere-else';
+ await waitFor(() => {
+ renderHelper()
+ })
+ `,
+ errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderHelper } from 'somewhere-else';
+ await waitFor(() => {
+ const { container } = renderHelper()
+ })
+ `,
+ errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderHelper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderHelper } from 'somewhere-else';
+ let container;
+ await waitFor(() => {
+ ({ container } = renderHelper())
+ })
+ `,
+ errors: [{ line: 6, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => result = render())
+ `,
+ errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => (a = 5, result = render()))
+ `,
+ errors: [{ line: 3, column: 30, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ const { rerender } = render()
+ await waitFor(() => rerender())
+ `,
+ errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor, render } from '@testing-library/react';
+ await waitFor(() => render())
+ `,
+ errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ const { rerender } = render()
+ await waitFor(() => rerender())
+ `,
+ errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => renderHelper())
+ `,
+ errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { render } from 'somewhere-else';
+ await waitFor(() => render())
+ `,
+ errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ settings: { 'testing-library/utils-module': '~/test-utils' },
+ code: `
+ import { waitFor, render } from '~/test-utils';
+ await waitFor(() => render())
+ `,
+ errors: [{ line: 3, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ settings: { 'testing-library/custom-renders': ['renderWrapper'] },
+ code: `
+ import { waitFor } from '@testing-library/react';
+ import { renderWrapper } from 'somewhere-else';
+ await waitFor(() => renderWrapper())
+ `,
+ errors: [{ line: 4, column: 29, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => {
+ render()
+ })
+ `,
+ errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => {
+ const { container } = render()
+ })
+ `,
+ errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => {
+ result = render()
+ })
+ `,
+ errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => {
+ const a = 5,
+ { container } = render()
+ })
+ `,
+ errors: [{ line: 4, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ const { rerender } = render()
+ await waitFor(() => {
+ rerender()
+ })
+ `,
+ errors: [{ line: 5, column: 11, messageId: 'noSideEffectsWaitFor' }],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => {
+ render()
+ fireEvent.keyDown(input, {key: 'ArrowDown'})
+ })
+ `,
+ errors: [
+ { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' },
+ { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' },
+ ],
+ },
+ {
+ code: `
+ import { waitFor } from '@testing-library/react';
+ await waitFor(() => {
+ render()
+ userEvent.click(button)
+ })
+ `,
+ errors: [
+ { line: 4, column: 11, messageId: 'noSideEffectsWaitFor' },
+ { line: 5, column: 11, messageId: 'noSideEffectsWaitFor' },
+ ],
+ },
// fireEvent
{
code: `