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
69 changes: 54 additions & 15 deletions lib/rules/no-unnecessary-act.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
getStatementCallExpression,
isEmptyFunction,
isExpressionStatement,
isMemberExpression,
isReturnStatement,
} from '../node-utils';
import { resolveToTestingLibraryFn } from '../utils';

import type { TSESTree } from '@typescript-eslint/utils';

Expand Down Expand Up @@ -58,6 +60,8 @@ export default createTestingLibraryRule<Options, MessageIds>({
],

create(context, [{ isStrict = true }], helpers) {
const userEventInstanceNames = new Set<string>();

function getStatementIdentifier(statement: TSESTree.Statement) {
const callExpression = getStatementCallExpression(statement);

Expand Down Expand Up @@ -87,11 +91,18 @@ export default createTestingLibraryRule<Options, MessageIds>({
return null;
}

/**
* Determines whether some call is non Testing Library related for a given list of statements.
*/
function hasSomeNonTestingLibraryCall(
statements: TSESTree.Statement[]
function hasUserEventInstanceName(identifier: TSESTree.Identifier) {
if (!isMemberExpression(identifier.parent)) {
return false;
}

const propertyIdentifier = getPropertyIdentifierNode(identifier.parent);
return userEventInstanceNames.has(propertyIdentifier?.name ?? '');
}

function hasStatementReference(
statements: TSESTree.Statement[],
predicate: (identifier: TSESTree.Identifier) => boolean
): boolean {
return statements.some((statement) => {
const identifier = getStatementIdentifier(statement);
Expand All @@ -100,20 +111,31 @@ export default createTestingLibraryRule<Options, MessageIds>({
return false;
}

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

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

if (!identifier) {
return false;
}
/**
* Determines whether some call is non Testing Library related for a given list of statements.
*/
function hasSomeNonTestingLibraryCall(
statements: TSESTree.Statement[]
): boolean {
return hasStatementReference(
statements,
(identifier) =>
!helpers.isTestingLibraryUtil(identifier) &&
!hasUserEventInstanceName(identifier)
);
}

return helpers.isTestingLibraryUtil(identifier);
});
function hasTestingLibraryCall(statements: TSESTree.Statement[]) {
return hasStatementReference(
statements,
(identifier) =>
helpers.isTestingLibraryUtil(identifier) ||
hasUserEventInstanceName(identifier)
);
}

function checkNoUnnecessaryActFromBlockStatement(
Expand Down Expand Up @@ -196,7 +218,24 @@ export default createTestingLibraryRule<Options, MessageIds>({
});
}

function registerUserEventInstance(
node: TSESTree.CallExpression & { parent: TSESTree.VariableDeclarator }
) {
const propertyIdentifier = getPropertyIdentifierNode(node);
const deepestIdentifier = getDeepestIdentifierNode(node);
const testingLibraryFn = resolveToTestingLibraryFn(node, context);

if (
propertyIdentifier?.name === testingLibraryFn?.local &&
deepestIdentifier?.name === 'setup' &&
ASTUtils.isIdentifier(node.parent?.id)
) {
userEventInstanceNames.add(node.parent.id.name);
}
}

return {
'VariableDeclarator > CallExpression': registerUserEventInstance,
'CallExpression > ArrowFunctionExpression > BlockStatement':
checkNoUnnecessaryActFromBlockStatement,
'CallExpression > FunctionExpression > BlockStatement':
Expand Down
71 changes: 71 additions & 0 deletions tests/lib/rules/no-unnecessary-act.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ const validNonStrictTestCases: RuleValidTestCase[] = [
});
`,
},
{
code: `// case: RTL act wrapping both userEvent and non-RTL calls
import { act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('valid case', async () => {
const user = userEvent.setup();

await act(async () => {
await user.click(element);
stuffThatDoesNotUseRTL()
});
})
`,
},
];

const validTestCases: RuleValidTestCase[] = [
Expand Down Expand Up @@ -245,6 +260,28 @@ const invalidStrictTestCases: RuleInvalidTestCase[] =
},
],
},
{
code: `
import { act } from '${testingFramework}'
import userEvent from '@testing-library/user-event'

test('invalid case', async () => {
const user = userEvent.setup();

await act(async () => {
await user.click(element);
stuffThatDoesNotUseRTL()
});
})
`,
errors: [
{
messageId: 'noUnnecessaryActTestingLibraryUtil',
line: 8,
column: 15,
},
],
},
]);

const invalidTestCases: RuleInvalidTestCase[] = [
Expand Down Expand Up @@ -566,6 +603,40 @@ const invalidTestCases: RuleInvalidTestCase[] = [
{ messageId: 'noUnnecessaryActEmptyFunction', line: 8, column: 9 },
],
},
{
code: `
import { act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('invalid case', async () => {
const user = userEvent.setup();

await act(async () => {
await user.click(element);
});
})
`,
errors: [
{ messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 15 },
],
},
{
code: `
import { act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('invalid case', async () => {
const user = userEvent.setup();

act(async () => {
await user.click(element);
});
})
`,
errors: [
{ messageId: 'noUnnecessaryActTestingLibraryUtil', line: 8, column: 9 },
],
},
{
settings: {
'testing-library/utils-module': 'test-utils',
Expand Down