Skip to content

feat(prefer-presence-queries): Add autofix support #1020

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 3 commits into from
Jun 5, 2025
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ module.exports = [
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than standalone queries | | | |
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `find(All)By*` query instead of `waitFor` + `get(All)By*` to wait for elements | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 |
| [prefer-implicit-assert](docs/rules/prefer-implicit-assert.md) | Suggest using implicit assertions for getBy* & findBy* queries | | | |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Ensure appropriate `get*`/`query*` queries are used with their respective matchers | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 |
| [prefer-query-by-disappearance](docs/rules/prefer-query-by-disappearance.md) | Suggest using `queryBy*` queries when waiting for disappearance | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | |
| [prefer-query-matchers](docs/rules/prefer-query-matchers.md) | Ensure the configured `get*`/`query*` query is used with the corresponding matchers | | | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using `screen` while querying | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | |
Expand Down
2 changes: 2 additions & 0 deletions docs/rules/prefer-presence-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

The (DOM) Testing Library allows to query DOM elements using different types of queries such as `get*` and `query*`. Using `get*` throws an error in case the element is not found, while `query*` returns null instead of throwing (or empty array for `queryAllBy*` ones). These differences are useful in some situations:
Expand Down
18 changes: 15 additions & 3 deletions lib/rules/prefer-presence-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
wrongAbsenceQuery:
'Use `queryBy*` queries rather than `getBy*` for checking element is NOT present',
},
fixable: 'code',
schema: [
{
type: 'object',
Expand Down Expand Up @@ -62,7 +63,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
const expectCallNode = findClosestCallNode(node, 'expect');
const withinCallNode = findClosestCallNode(node, 'within');

if (!expectCallNode || !isMemberExpression(expectCallNode.parent)) {
if (!isMemberExpression(expectCallNode?.parent)) {
return;
}

Expand All @@ -86,14 +87,25 @@ export default createTestingLibraryRule<Options, MessageIds>({
(withinCallNode || isPresenceAssert) &&
!isPresenceQuery
) {
context.report({ node, messageId: 'wrongPresenceQuery' });
const newQueryName = node.name.replace(/^query/, 'get');

context.report({
node,
messageId: 'wrongPresenceQuery',
fix: (fixer) => fixer.replaceText(node, newQueryName),
});
} else if (
!withinCallNode &&
absence &&
isAbsenceAssert &&
isPresenceQuery
) {
context.report({ node, messageId: 'wrongAbsenceQuery' });
const newQueryName = node.name.replace(/^get/, 'query');
context.report({
node,
messageId: 'wrongAbsenceQuery',
fix: (fixer) => fixer.replaceText(node, newQueryName),
});
}
},
};
Expand Down
96 changes: 95 additions & 1 deletion tests/lib/rules/prefer-presence-queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,34 @@ const getDisabledValidAssertion = ({
};
};

const toggleQueryPrefix = (query: string): string => {
if (query.startsWith('get')) return query.replace(/^get/, 'query');
if (query.startsWith('query')) return query.replace(/^query/, 'get');
return query;
};

const applyScreenPrefix = (query: string, shouldUseScreen: boolean): string =>
shouldUseScreen ? `screen.${query}` : query;

const getInvalidAssertions = ({
query,
matcher,
messageId,
shouldUseScreen = false,
assertionType,
}: AssertionFnParams): RuleInvalidTestCase[] => {
const finalQuery = shouldUseScreen ? `screen.${query}` : query;
const finalQuery = applyScreenPrefix(query, shouldUseScreen);
const code = `expect(${finalQuery}('Hello'))${matcher}`;

const outputQuery = toggleQueryPrefix(query);
const finalOutputQuery = applyScreenPrefix(outputQuery, shouldUseScreen);
const output = `expect(${finalOutputQuery}('Hello'))${matcher}`;

return [
{
code,
errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }],
output,
},
{
code,
Expand All @@ -105,6 +120,7 @@ const getInvalidAssertions = ({
},
],
errors: [{ messageId, line: 1, column: shouldUseScreen ? 15 : 8 }],
output,
},
];
};
Expand Down Expand Up @@ -1307,46 +1323,68 @@ ruleTester.run(RULE_NAME, rule, {
{
code: 'expect(screen.getAllByText("button")[1]).not.toBeInTheDocument()',
errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }],
output:
'expect(screen.queryAllByText("button")[1]).not.toBeInTheDocument()',
},
{
code: 'expect(screen.getAllByText("button")[1]).not.toBeOnTheScreen()',
errors: [{ messageId: 'wrongAbsenceQuery', line: 1, column: 15 }],
output:
'expect(screen.queryAllByText("button")[1]).not.toBeOnTheScreen()',
},
{
code: 'expect(screen.queryAllByText("button")[1]).toBeInTheDocument()',
errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }],
output: 'expect(screen.getAllByText("button")[1]).toBeInTheDocument()',
},
{
code: 'expect(screen.queryAllByText("button")[1]).toBeOnTheScreen()',
errors: [{ messageId: 'wrongPresenceQuery', line: 1, column: 15 }],
output: 'expect(screen.getAllByText("button")[1]).toBeOnTheScreen()',
},
{
code: `
// case: asserting presence incorrectly with custom queryBy* query
expect(queryByCustomQuery("button")).toBeInTheDocument()
`,
errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }],
output: `
// case: asserting presence incorrectly with custom queryBy* query
expect(getByCustomQuery("button")).toBeInTheDocument()
`,
},
{
code: `
// case: asserting presence incorrectly with custom queryBy* query
expect(queryByCustomQuery("button")).toBeOnTheScreen()
`,
errors: [{ messageId: 'wrongPresenceQuery', line: 3, column: 16 }],
output: `
// case: asserting presence incorrectly with custom queryBy* query
expect(getByCustomQuery("button")).toBeOnTheScreen()
`,
},
{
code: `
// case: asserting absence incorrectly with custom getBy* query
expect(getByCustomQuery("button")).not.toBeInTheDocument()
`,
errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }],
output: `
// case: asserting absence incorrectly with custom getBy* query
expect(queryByCustomQuery("button")).not.toBeInTheDocument()
`,
},
{
code: `
// case: asserting absence incorrectly with custom getBy* query
expect(getByCustomQuery("button")).not.toBeOnTheScreen()
`,
errors: [{ messageId: 'wrongAbsenceQuery', line: 3, column: 16 }],
output: `
// case: asserting absence incorrectly with custom getBy* query
expect(queryByCustomQuery("button")).not.toBeOnTheScreen()
`,
},
{
settings: {
Expand All @@ -1358,6 +1396,11 @@ ruleTester.run(RULE_NAME, rule, {
expect(queryByRole("button")).toBeInTheDocument()
`,
errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting presence incorrectly importing custom module
import 'test-utils'
expect(getByRole("button")).toBeInTheDocument()
`,
},
{
settings: {
Expand All @@ -1369,6 +1412,11 @@ ruleTester.run(RULE_NAME, rule, {
expect(queryByRole("button")).toBeOnTheScreen()
`,
errors: [{ line: 4, column: 14, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting presence incorrectly importing custom module
import 'test-utils'
expect(getByRole("button")).toBeOnTheScreen()
`,
},
{
settings: {
Expand All @@ -1380,6 +1428,11 @@ ruleTester.run(RULE_NAME, rule, {
expect(getByRole("button")).not.toBeInTheDocument()
`,
errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }],
output: `
// case: asserting absence incorrectly importing custom module
import 'test-utils'
expect(queryByRole("button")).not.toBeInTheDocument()
`,
},
{
settings: {
Expand All @@ -1391,18 +1444,29 @@ ruleTester.run(RULE_NAME, rule, {
expect(getByRole("button")).not.toBeOnTheScreen()
`,
errors: [{ line: 4, column: 14, messageId: 'wrongAbsenceQuery' }],
output: `
// case: asserting absence incorrectly importing custom module
import 'test-utils'
expect(queryByRole("button")).not.toBeOnTheScreen()
`,
},
{
code: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).getByText("Hello")).not.toBeInTheDocument()`,
errors: [{ line: 3, column: 46, messageId: 'wrongAbsenceQuery' }],
output: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`,
},
{
code: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).queryByText("Hello")).toBeInTheDocument()`,
errors: [{ line: 3, column: 46, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`,
},
{
code: `
Expand All @@ -1412,18 +1476,27 @@ ruleTester.run(RULE_NAME, rule, {
{ line: 3, column: 25, messageId: 'wrongPresenceQuery' },
{ line: 3, column: 48, messageId: 'wrongAbsenceQuery' },
],
output: `
// case: asserting within check does still work with improper outer clause and improper inner clause
expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`,
},
{
code: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.queryByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`,
errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeInTheDocument()`,
},
{
code: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.queryByRole("button")).getByText("Hello")).toBeInTheDocument()`,
errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`,
},
{
code: `
Expand All @@ -1433,18 +1506,27 @@ ruleTester.run(RULE_NAME, rule, {
{ line: 3, column: 25, messageId: 'wrongPresenceQuery' },
{ line: 3, column: 48, messageId: 'wrongPresenceQuery' },
],
output: `
// case: asserting within check does still work with improper outer clause and improper inner clause
expect(within(screen.getByRole("button")).getByText("Hello")).toBeInTheDocument()`,
},
{
code: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).getByText("Hello")).not.toBeOnTheScreen()`,
errors: [{ line: 3, column: 46, messageId: 'wrongAbsenceQuery' }],
output: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`,
},
{
code: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).queryByText("Hello")).toBeOnTheScreen()`,
errors: [{ line: 3, column: 46, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting within check does still work with improper outer clause
expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`,
},
{
code: `
Expand All @@ -1454,18 +1536,27 @@ ruleTester.run(RULE_NAME, rule, {
{ line: 3, column: 25, messageId: 'wrongPresenceQuery' },
{ line: 3, column: 48, messageId: 'wrongAbsenceQuery' },
],
output: `
// case: asserting within check does still work with improper outer clause and improper inner clause
expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`,
},
{
code: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.queryByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`,
errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.getByRole("button")).queryByText("Hello")).not.toBeOnTheScreen()`,
},
{
code: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.queryByRole("button")).getByText("Hello")).toBeOnTheScreen()`,
errors: [{ line: 3, column: 25, messageId: 'wrongPresenceQuery' }],
output: `
// case: asserting within check does still work with proper outer clause and improper inner clause
expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`,
},
{
code: `
Expand All @@ -1475,6 +1566,9 @@ ruleTester.run(RULE_NAME, rule, {
{ line: 3, column: 25, messageId: 'wrongPresenceQuery' },
{ line: 3, column: 48, messageId: 'wrongPresenceQuery' },
],
output: `
// case: asserting within check does still work with improper outer clause and improper inner clause
expect(within(screen.getByRole("button")).getByText("Hello")).toBeOnTheScreen()`,
},
],
});