Skip to content
9 changes: 9 additions & 0 deletions docs/rules/prefer-find-by.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ const submitButton = await waitFor(() =>
const submitButton = await waitFor(() =>
queryAllByText('button', { name: /submit/i })
);

// arrow functions with one statement, calling any sync query method with presence assertion
const submitButton = await waitFor(() =>
expect(queryByLabel('button', { name: /submit/i })).toBeInTheDocument()
);

const submitButton = await waitFor(() =>
expect(queryByLabel('button', { name: /submit/i })).not.toBeFalsy()
);
```

Examples of **correct** code for this rule:
Expand Down
267 changes: 248 additions & 19 deletions lib/rules/prefer-find-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,226 @@ export default createTestingLibraryRule<Options, MessageIds>({
});
}

function getWrongQueryNameInAssertion(
node: TSESTree.ArrowFunctionExpression
) {
if (
!isCallExpression(node.body) ||
!isMemberExpression(node.body.callee)
) {
return null;
}

// expect(getByText).toBeInTheDocument() shape
if (
isCallExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.arguments[0]) &&
ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee)
) {
return node.body.callee.object.arguments[0].callee.name;
}

if (!ASTUtils.isIdentifier(node.body.callee.property)) {
return null;
}

// expect(screen.getByText).toBeInTheDocument() shape
if (
isCallExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.arguments[0]) &&
isMemberExpression(node.body.callee.object.arguments[0].callee) &&
ASTUtils.isIdentifier(
node.body.callee.object.arguments[0].callee.property
)
) {
return node.body.callee.object.arguments[0].callee.property.name;
}

// expect(screen.getByText).not shape
if (
isMemberExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.object) &&
isCallExpression(node.body.callee.object.object.arguments[0]) &&
isMemberExpression(
node.body.callee.object.object.arguments[0].callee
) &&
ASTUtils.isIdentifier(
node.body.callee.object.object.arguments[0].callee.property
)
) {
return node.body.callee.object.object.arguments[0].callee.property.name;
}

// expect(getByText).not shape
if (
isMemberExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.object) &&
isCallExpression(node.body.callee.object.object.arguments[0]) &&
ASTUtils.isIdentifier(
node.body.callee.object.object.arguments[0].callee
)
) {
return node.body.callee.object.object.arguments[0].callee.name;
}

return node.body.callee.property.name;
}

function getWrongQueryName(node: TSESTree.ArrowFunctionExpression) {
if (!isCallExpression(node.body)) {
return null;
}

// expect(() => getByText) and expect(() => screen.getByText) shape
if (
ASTUtils.isIdentifier(node.body.callee) &&
helpers.isSyncQuery(node.body.callee)
) {
return node.body.callee.name;
}

return getWrongQueryNameInAssertion(node);
}

function getCaller(node: TSESTree.ArrowFunctionExpression) {
if (
!isCallExpression(node.body) ||
!isMemberExpression(node.body.callee)
) {
return null;
}

if (ASTUtils.isIdentifier(node.body.callee.object)) {
// () => screen.getByText
return node.body.callee.object.name;
} else if (
// expect()
isCallExpression(node.body.callee.object) &&
ASTUtils.isIdentifier(node.body.callee.object.callee) &&
isCallExpression(node.body.callee.object.arguments[0]) &&
isMemberExpression(node.body.callee.object.arguments[0].callee) &&
ASTUtils.isIdentifier(
node.body.callee.object.arguments[0].callee.object
)
) {
return node.body.callee.object.arguments[0].callee.object.name;
} else if (
// expect().not
isMemberExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.object) &&
isCallExpression(node.body.callee.object.object.arguments[0]) &&
isMemberExpression(
node.body.callee.object.object.arguments[0].callee
) &&
ASTUtils.isIdentifier(
node.body.callee.object.object.arguments[0].callee.object
)
) {
return node.body.callee.object.object.arguments[0].callee.object.name;
}

return null;
}

function isSyncQuery(node: TSESTree.ArrowFunctionExpression) {
if (!isCallExpression(node.body)) {
return false;
}

const isQuery =
ASTUtils.isIdentifier(node.body.callee) && // () => getByText
helpers.isSyncQuery(node.body.callee);

const isWrappedInPresenceAssert =
isMemberExpression(node.body.callee) &&
isCallExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.arguments[0]) &&
ASTUtils.isIdentifier(node.body.callee.object.arguments[0].callee) &&
helpers.isSyncQuery(node.body.callee.object.arguments[0].callee) &&
helpers.isPresenceAssert(node.body.callee);

const isWrappedInNegatedPresenceAssert =
isMemberExpression(node.body.callee) && // wrpaped in presence expect().not
isMemberExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.object) &&
isCallExpression(node.body.callee.object.object.arguments[0]) &&
ASTUtils.isIdentifier(
node.body.callee.object.object.arguments[0].callee
) &&
helpers.isSyncQuery(
node.body.callee.object.object.arguments[0].callee
) &&
helpers.isPresenceAssert(node.body.callee.object);

return (
isQuery || isWrappedInPresenceAssert || isWrappedInNegatedPresenceAssert
);
}

function isScreenSyncQuery(node: TSESTree.ArrowFunctionExpression) {
if (!isArrowFunctionExpression(node) || !isCallExpression(node.body)) {
return false;
}

if (
!isMemberExpression(node.body.callee) ||
!ASTUtils.isIdentifier(node.body.callee.property)
) {
return false;
}

if (
!ASTUtils.isIdentifier(node.body.callee.object) &&
!isCallExpression(node.body.callee.object) &&
!isMemberExpression(node.body.callee.object)
) {
return false;
}

const isWrappedInPresenceAssert =
helpers.isPresenceAssert(node.body.callee) &&
isCallExpression(node.body.callee.object) &&
isCallExpression(node.body.callee.object.arguments[0]) &&
isMemberExpression(node.body.callee.object.arguments[0].callee) &&
ASTUtils.isIdentifier(
node.body.callee.object.arguments[0].callee.object
);

const isWrappedInNegatedPresenceAssert =
isMemberExpression(node.body.callee.object) &&
helpers.isPresenceAssert(node.body.callee.object) &&
isCallExpression(node.body.callee.object.object) &&
isCallExpression(node.body.callee.object.object.arguments[0]) &&
isMemberExpression(node.body.callee.object.object.arguments[0].callee);

return (
helpers.isSyncQuery(node.body.callee.property) ||
isWrappedInPresenceAssert ||
isWrappedInNegatedPresenceAssert
);
}

function getQueryArguments(node: TSESTree.CallExpression) {
if (
isMemberExpression(node.callee) &&
isCallExpression(node.callee.object) &&
isCallExpression(node.callee.object.arguments[0])
) {
return node.callee.object.arguments[0].arguments;
}

if (
isMemberExpression(node.callee) &&
isMemberExpression(node.callee.object) &&
isCallExpression(node.callee.object.object) &&
isCallExpression(node.callee.object.object.arguments[0])
) {
return node.callee.object.object.arguments[0].arguments;
}

return node.arguments;
}

return {
'AwaitExpression > CallExpression'(node: TSESTree.CallExpression) {
if (
Expand All @@ -122,27 +342,32 @@ export default createTestingLibraryRule<Options, MessageIds>({
// ensure the only argument is an arrow function expression - if the arrow function is a block
// we skip it
const argument = node.arguments[0];
if (!isArrowFunctionExpression(argument)) {
return;
}
if (!isCallExpression(argument.body)) {
if (
!isArrowFunctionExpression(argument) ||
!isCallExpression(argument.body)
) {
return;
}

const waitForMethodName = node.callee.name;

// ensure here it's one of the sync methods that we are calling
if (
isMemberExpression(argument.body.callee) &&
ASTUtils.isIdentifier(argument.body.callee.property) &&
ASTUtils.isIdentifier(argument.body.callee.object) &&
helpers.isSyncQuery(argument.body.callee.property)
) {
if (isScreenSyncQuery(argument)) {
const caller = getCaller(argument);

if (!caller) {
return;
}

// shape of () => screen.getByText
const fullQueryMethod = argument.body.callee.property.name;
const caller = argument.body.callee.object.name;
const fullQueryMethod = getWrongQueryName(argument);

if (!fullQueryMethod) {
return;
}

const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = argument.body.arguments;
const callArguments = getQueryArguments(argument.body);
const queryMethod = fullQueryMethod.split('By')[1];

reportInvalidUsage(node, {
Expand All @@ -166,17 +391,21 @@ export default createTestingLibraryRule<Options, MessageIds>({
});
return;
}
if (
!ASTUtils.isIdentifier(argument.body.callee) ||
!helpers.isSyncQuery(argument.body.callee)
) {

if (!isSyncQuery(argument)) {
return;
}

// shape of () => getByText
const fullQueryMethod = argument.body.callee.name;
const fullQueryMethod = getWrongQueryName(argument);

if (!fullQueryMethod) {
return;
}

const queryMethod = fullQueryMethod.split('By')[1];
const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = argument.body.arguments;
const callArguments = getQueryArguments(argument.body);

reportInvalidUsage(node, {
queryMethod,
Expand Down
Loading