Skip to content

Commit 95a3a9b

Browse files
committed
feat(prefer-presence-queries): use aggressive query reporting
1 parent 08d6d3b commit 95a3a9b

File tree

6 files changed

+103
-82
lines changed

6 files changed

+103
-82
lines changed

.lintstagedrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"*.{js,ts}": [
3-
"eslint --fix",
3+
"eslint --max-warnings 0 --fix",
44
"prettier --write",
55
"jest --findRelatedTests"
66
],

lib/detect-testing-library-utils.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
1+
import { ASTUtils, TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
22
import {
33
getImportModuleName,
44
isLiteral,
55
ImportModuleNode,
66
isImportDeclaration,
77
isImportNamespaceSpecifier,
88
isImportSpecifier,
9-
isIdentifier,
109
isProperty,
1110
} from './node-utils';
1211

@@ -41,6 +40,9 @@ export type DetectionHelpers = {
4140
getCustomModuleImportName: () => string | undefined;
4241
getIsTestingLibraryImported: () => boolean;
4342
getIsValidFilename: () => boolean;
43+
isGetByQuery: (node: TSESTree.Identifier) => boolean;
44+
isQueryByQuery: (node: TSESTree.Identifier) => boolean;
45+
isSyncQuery: (node: TSESTree.Identifier) => boolean;
4446
canReportErrors: () => boolean;
4547
findImportedUtilSpecifier: (
4648
specifierName: string
@@ -85,7 +87,8 @@ export function detectTestingLibraryUtils<
8587
return getImportModuleName(importedCustomModuleNode);
8688
},
8789
/**
88-
* Gets if Testing Library is considered as imported or not.
90+
* Determines whether Testing Library utils are imported or not for
91+
* current file being analyzed.
8992
*
9093
* By default, it is ALWAYS considered as imported. This is what we call
9194
* "aggressive reporting" so we don't miss TL utils reexported from
@@ -105,17 +108,37 @@ export function detectTestingLibraryUtils<
105108
},
106109

107110
/**
108-
* Gets if filename being analyzed is valid or not.
109-
*
110-
* This is based on "testing-library/filename-pattern" setting.
111+
* Determines whether filename is valid or not for current file
112+
* being analyzed based on "testing-library/filename-pattern" setting.
111113
*/
112114
getIsValidFilename() {
113115
const fileName = context.getFilename();
114116
return !!fileName.match(filenamePattern);
115117
},
116118

117119
/**
118-
* Wraps all conditions that must be met to report rules.
120+
* Determines whether a given node is `getBy*` or `getAllBy*` query variant or not.
121+
*/
122+
isGetByQuery(node) {
123+
return !!node.name.match(/^get(All)?By.+$/);
124+
},
125+
126+
/**
127+
* Determines whether a given node is `queryBy*` or `queryAllBy*` query variant or not.
128+
*/
129+
isQueryByQuery(node) {
130+
return !!node.name.match(/^query(All)?By.+$/);
131+
},
132+
133+
/**
134+
* Determines whether a given node is sync query.
135+
*/
136+
isSyncQuery(node) {
137+
return this.isGetByQuery(node) || this.isQueryByQuery(node);
138+
},
139+
140+
/**
141+
* Determines if file inspected meets all conditions to be reported by rules or not.
119142
*/
120143
canReportErrors() {
121144
return (
@@ -144,7 +167,7 @@ export function detectTestingLibraryUtils<
144167
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
145168
} else {
146169
const requireNode = node.parent as TSESTree.VariableDeclarator;
147-
if (isIdentifier(requireNode.id)) {
170+
if (ASTUtils.isIdentifier(requireNode.id)) {
148171
// this is const rtl = require('foo')
149172
return requireNode.id;
150173
}
@@ -153,7 +176,7 @@ export function detectTestingLibraryUtils<
153176
const property = destructuring.properties.find(
154177
(n) =>
155178
isProperty(n) &&
156-
isIdentifier(n.key) &&
179+
ASTUtils.isIdentifier(n.key) &&
157180
n.key.name === specifierName
158181
);
159182
return (property as TSESTree.Property).key as TSESTree.Identifier;

lib/rules/prefer-presence-queries.ts

Lines changed: 42 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,12 @@
1-
import { TSESTree } from '@typescript-eslint/experimental-utils';
2-
import {
3-
ABSENCE_MATCHERS,
4-
ALL_QUERIES_METHODS,
5-
PRESENCE_MATCHERS,
6-
} from '../utils';
7-
import {
8-
findClosestCallNode,
9-
isIdentifier,
10-
isMemberExpression,
11-
} from '../node-utils';
1+
import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from '../utils';
3+
import { findClosestCallNode, isMemberExpression } from '../node-utils';
124
import { createTestingLibraryRule } from '../create-testing-library-rule';
135

146
export const RULE_NAME = 'prefer-presence-queries';
157
export type MessageIds = 'presenceQuery' | 'absenceQuery';
168
type Options = [];
179

18-
const QUERIES_REGEXP = new RegExp(
19-
`^(get|query)(All)?(${ALL_QUERIES_METHODS.join('|')})$`
20-
);
21-
22-
function isThrowingQuery(node: TSESTree.Identifier) {
23-
return node.name.startsWith('get');
24-
}
25-
2610
export default createTestingLibraryRule<Options, MessageIds>({
2711
name: RULE_NAME,
2812
meta: {
@@ -44,49 +28,53 @@ export default createTestingLibraryRule<Options, MessageIds>({
4428
},
4529
defaultOptions: [],
4630

47-
create(context) {
31+
create(context, _, helpers) {
4832
return {
49-
[`CallExpression Identifier[name=${QUERIES_REGEXP}]`](
50-
node: TSESTree.Identifier
51-
) {
33+
'CallExpression Identifier'(node: TSESTree.Identifier) {
5234
const expectCallNode = findClosestCallNode(node, 'expect');
5335

54-
if (expectCallNode && isMemberExpression(expectCallNode.parent)) {
55-
const expectStatement = expectCallNode.parent;
56-
const property = expectStatement.property as TSESTree.Identifier;
57-
let matcher = property.name;
58-
let isNegatedMatcher = false;
36+
if (!expectCallNode || !isMemberExpression(expectCallNode.parent)) {
37+
return;
38+
}
5939

60-
if (
61-
matcher === 'not' &&
62-
isMemberExpression(expectStatement.parent) &&
63-
isIdentifier(expectStatement.parent.property)
64-
) {
65-
isNegatedMatcher = true;
66-
matcher = expectStatement.parent.property.name;
67-
}
40+
if (!helpers.isSyncQuery(node)) {
41+
return;
42+
}
43+
44+
const isPresenceQuery = helpers.isGetByQuery(node);
45+
const expectStatement = expectCallNode.parent;
46+
let matcher =
47+
ASTUtils.isIdentifier(expectStatement.property) &&
48+
expectStatement.property.name;
49+
let isNegatedMatcher = false;
50+
51+
if (
52+
matcher === 'not' &&
53+
isMemberExpression(expectStatement.parent) &&
54+
ASTUtils.isIdentifier(expectStatement.parent.property)
55+
) {
56+
isNegatedMatcher = true;
57+
matcher = expectStatement.parent.property.name;
58+
}
6859

69-
const validMatchers = isThrowingQuery(node)
70-
? PRESENCE_MATCHERS
71-
: ABSENCE_MATCHERS;
60+
const validMatchers = isPresenceQuery
61+
? PRESENCE_MATCHERS
62+
: ABSENCE_MATCHERS;
7263

73-
const invalidMatchers = isThrowingQuery(node)
74-
? ABSENCE_MATCHERS
75-
: PRESENCE_MATCHERS;
64+
const invalidMatchers = isPresenceQuery
65+
? ABSENCE_MATCHERS
66+
: PRESENCE_MATCHERS;
7667

77-
const messageId = isThrowingQuery(node)
78-
? 'absenceQuery'
79-
: 'presenceQuery';
68+
const messageId = isPresenceQuery ? 'absenceQuery' : 'presenceQuery';
8069

81-
if (
82-
(!isNegatedMatcher && invalidMatchers.includes(matcher)) ||
83-
(isNegatedMatcher && validMatchers.includes(matcher))
84-
) {
85-
return context.report({
86-
node,
87-
messageId,
88-
});
89-
}
70+
if (
71+
(!isNegatedMatcher && invalidMatchers.includes(matcher)) ||
72+
(isNegatedMatcher && validMatchers.includes(matcher))
73+
) {
74+
context.report({
75+
node,
76+
messageId,
77+
});
9078
}
9179
},
9280
};

lib/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const LIBRARY_MODULES = [
2424
'@testing-library/svelte',
2525
];
2626

27+
// TODO: should be deleted after all rules are migrated to v4
2728
const hasTestingLibraryImportModule = (
2829
node: TSESTree.ImportDeclaration
2930
): boolean => {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"scripts": {
2828
"build": "tsc",
2929
"postbuild": "cpy README.md ./dist && cpy package.json ./dist && cpy LICENSE ./dist",
30-
"lint": "eslint . --ext .js,.ts",
30+
"lint": "eslint . --max-warnings 0 --ext .js,.ts",
3131
"lint:fix": "npm run lint -- --fix",
3232
"format": "prettier --write README.md \"{lib,docs,tests}/**/*.{js,ts,md}\"",
3333
"format:check": "prettier --check README.md \"{lib,docs,tests}/**/*.{js,json,yml,ts,md}\"",

tests/lib/rules/prefer-presence-queries.test.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -346,29 +346,25 @@ ruleTester.run(RULE_NAME, rule, {
346346
{
347347
code: 'const el = queryByText("button")',
348348
},
349-
{
350-
// TODO: this one is gonna be reported by aggressive query reporting
351-
code:
352-
'expect(getByNonTestingLibraryQuery("button")).not.toBeInTheDocument()',
353-
},
354-
{
355-
// TODO: this one is gonna be reported by aggressive query reporting
356-
code:
357-
'expect(queryByNonTestingLibraryQuery("button")).toBeInTheDocument()',
358-
},
359349
{
360350
code: `async () => {
361351
const el = await findByText('button')
362352
expect(el).toBeInTheDocument()
363353
}`,
364354
},
355+
`// case: query an element with getBy but then check its absence after doing
356+
// some action which makes it disappear.
357+
358+
// submit button exists
359+
const submitButton = screen.getByRole('button')
360+
fireEvent.click(submitButton)
361+
362+
// right after clicking submit button it disappears
363+
expect(submitButton).not.toBeInTheDocument()
364+
`,
365365
// some weird examples after here to check guard against parent nodes
366-
{
367-
code: 'expect(getByText("button")).not()',
368-
},
369-
{
370-
code: 'expect(queryByText("button")).not()',
371-
},
366+
'expect(getByText("button")).not()',
367+
'expect(queryByText("button")).not()',
372368
],
373369
invalid: [
374370
// cases: asserting absence incorrectly with `getBy*` queries
@@ -655,7 +651,20 @@ ruleTester.run(RULE_NAME, rule, {
655651
code: 'expect(screen.queryAllByText("button")[1]).toBeInTheDocument()',
656652
errors: [{ messageId: 'presenceQuery', line: 1, column: 15 }],
657653
},
658-
// TODO: add more tests for using custom queries
654+
{
655+
code: `
656+
// case: asserting presence incorrectly with custom queryBy* query
657+
expect(queryByCustomQuery("button")).toBeInTheDocument()
658+
`,
659+
errors: [{ messageId: 'presenceQuery', line: 3, column: 16 }],
660+
},
661+
{
662+
code: `
663+
// case: asserting absence incorrectly with custom getBy* query
664+
expect(getByCustomQuery("button")).not.toBeInTheDocument()
665+
`,
666+
errors: [{ messageId: 'absenceQuery', line: 3, column: 16 }],
667+
},
659668
// TODO: add more tests for importing custom module
660669
],
661670
});

0 commit comments

Comments
 (0)