Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions docs/rules/no-unnecessary-act.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ await act(async () => {
});
```

## Options

This rule has one option:

- `isStrict`: **disabled by default**. Wrapping both things related and not related to Testing Library in `act` is reported

```js
"testing-library/no-unnecessary-act": ["error", {"isStrict": true}]
```

Incorrect:

```jsx
// ❌ wrapping both things related and not related to Testing Library in `act` is NOT correct

import { act, screen } from '@testing-library/react';
import { stuffThatDoesNotUseRTL } from 'somwhere-else';

await act(async () => {
await screen.findByRole('button');
stuffThatDoesNotUseRTL();
});
```

## Further Reading

- [Inspiration for this rule](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#wrapping-things-in-act-unnecessarily)
Expand Down
61 changes: 52 additions & 9 deletions lib/rules/no-unnecessary-act.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export const RULE_NAME = 'no-unnecessary-act';
export type MessageIds =
| 'noUnnecessaryActEmptyFunction'
| 'noUnnecessaryActTestingLibraryUtil';
type Options = [{ isStrict: boolean }];

export default createTestingLibraryRule<[], MessageIds>({
export default createTestingLibraryRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'problem',
Expand All @@ -32,37 +33,71 @@ export default createTestingLibraryRule<[], MessageIds>({
'Avoid wrapping Testing Library util calls in `act`',
noUnnecessaryActEmptyFunction: 'Avoid wrapping empty function in `act`',
},
schema: [],
schema: [
{
type: 'object',
properties: {
isStrict: { type: 'boolean' },
},
},
],
},
defaultOptions: [],
defaultOptions: [
{
isStrict: false,
},
],

create(context, [options], helpers) {
function getStatementIdentifier(statement: TSESTree.Statement) {
const callExpression = getStatementCallExpression(statement);

if (!callExpression) {
return null;
}

const identifier = getDeepestIdentifierNode(callExpression);

if (!identifier) {
return null;
}

return identifier;
}

create(context, _, helpers) {
/**
* Determines whether some call is non Testing Library related for a given list of statements.
*/
function hasSomeNonTestingLibraryCall(
statements: TSESTree.Statement[]
): boolean {
return statements.some((statement) => {
const callExpression = getStatementCallExpression(statement);
const identifier = getStatementIdentifier(statement);

if (!callExpression) {
if (!identifier) {
return false;
}

const identifier = getDeepestIdentifierNode(callExpression);
return !helpers.isTestingLibraryUtil(identifier);
});
}

function hasTestingLibraryCall(statements: TSESTree.Statement[]) {
return statements.some((statement) => {
const identifier = getStatementIdentifier(statement);

if (!identifier) {
return false;
}

return !helpers.isTestingLibraryUtil(identifier);
return helpers.isTestingLibraryUtil(identifier);
});
}

function checkNoUnnecessaryActFromBlockStatement(
blockStatementNode: TSESTree.BlockStatement
) {
const { isStrict } = options;
const functionNode = blockStatementNode.parent as
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionExpression
Expand All @@ -89,7 +124,15 @@ export default createTestingLibraryRule<[], MessageIds>({
node: identifierNode,
messageId: 'noUnnecessaryActEmptyFunction',
});
} else if (!hasSomeNonTestingLibraryCall(blockStatementNode.body)) {
return;
}

const shouldBeReported = isStrict
? hasSomeNonTestingLibraryCall(blockStatementNode.body) &&
hasTestingLibraryCall(blockStatementNode.body)
: !hasSomeNonTestingLibraryCall(blockStatementNode.body);

if (shouldBeReported) {
context.report({
node: identifierNode,
messageId: 'noUnnecessaryActTestingLibraryUtil',
Expand Down
52 changes: 52 additions & 0 deletions tests/lib/rules/no-unnecessary-act.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,27 @@ ruleTester.run(RULE_NAME, rule, {
});
`,
},
{
options: [
{
isStrict: true,
},
],
code: `// case: RTL act wrapping non-RTL calls with strict option
import { act, render } from '@testing-library/react'

act(() => jest.advanceTimersByTime(1000))
act(() => {
jest.advanceTimersByTime(1000)
})
act(() => {
return jest.advanceTimersByTime(1000)
})
act(function() {
return jest.advanceTimersByTime(1000)
})
`,
},
],
invalid: [
// cases for act related to React Testing Library
Expand Down Expand Up @@ -799,5 +820,36 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
options: [
{
isStrict: true,
},
],
code: `// case: RTL act wrapping both RTL and non-RTL calls with strict option
import { act, render } from '@testing-library/react'

await act(async () => {
userEvent.click(screen.getByText("Submit"))
await flushPromises()
})
act(function() {
userEvent.click(screen.getByText("Submit"))
flushPromises()
})
`,
errors: [
{
messageId: 'noUnnecessaryActTestingLibraryUtil',
line: 4,
column: 13,
},
{
messageId: 'noUnnecessaryActTestingLibraryUtil',
line: 8,
column: 7,
},
],
},
],
});