Skip to content

Commit

Permalink
feat(no-unnecessary-act): add isStrict option (#404)
Browse files Browse the repository at this point in the history
* feat: isStrict option for no-unnecessary-act

* refactor: rename and simplify isReported -> shouldBeReported

* refactor: specify in docs that isStrict is disabled by default

Closes #382
  • Loading branch information
zaicevas committed Jul 6, 2021
1 parent b94437a commit b68d66b
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 9 deletions.
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,
},
],
},
],
});

0 comments on commit b68d66b

Please sign in to comment.