Skip to content

feat(prefer-find-by): report presence assertions #450

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 24, 2021
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