Skip to content

Commit f8b2b1f

Browse files
committed
refactor(extract helpers for detecting presence/absence assets): add fake rule tests for queries
1 parent 81d9487 commit f8b2b1f

5 files changed

+137
-7
lines changed

lib/detect-testing-library-utils.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getImportModuleName, ImportModuleNode, isLiteral } from './node-utils';
2+
import {
3+
getAssertNodeInfo,
4+
getImportModuleName,
5+
ImportModuleNode,
6+
isLiteral,
7+
} from './node-utils';
8+
import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from './utils';
39

410
export type TestingLibrarySettings = {
511
'testing-library/module'?: string;
@@ -35,6 +41,8 @@ export type DetectionHelpers = {
3541
isGetByQuery: (node: TSESTree.Identifier) => boolean;
3642
isQueryByQuery: (node: TSESTree.Identifier) => boolean;
3743
isSyncQuery: (node: TSESTree.Identifier) => boolean;
44+
isPresenceAssert: (node: TSESTree.MemberExpression) => boolean;
45+
isAbsenceAssert: (node: TSESTree.MemberExpression) => boolean;
3846
canReportErrors: () => boolean;
3947
};
4048

@@ -126,6 +134,30 @@ export function detectTestingLibraryUtils<
126134
return this.isGetByQuery(node) || this.isQueryByQuery(node);
127135
},
128136

137+
isPresenceAssert(node) {
138+
const { matcher, isNegated } = getAssertNodeInfo(node);
139+
140+
if (!matcher) {
141+
return false;
142+
}
143+
144+
return isNegated
145+
? ABSENCE_MATCHERS.includes(matcher)
146+
: PRESENCE_MATCHERS.includes(matcher);
147+
},
148+
149+
isAbsenceAssert(node) {
150+
const { matcher, isNegated } = getAssertNodeInfo(node);
151+
152+
if (!matcher) {
153+
return false;
154+
}
155+
156+
return isNegated
157+
? PRESENCE_MATCHERS.includes(matcher)
158+
: ABSENCE_MATCHERS.includes(matcher);
159+
},
160+
129161
/**
130162
* Determines if file inspected meets all conditions to be reported by rules or not.
131163
*/

lib/node-utils.ts

+40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
AST_NODE_TYPES,
3+
ASTUtils,
34
TSESLint,
45
TSESTree,
56
} from '@typescript-eslint/experimental-utils';
@@ -253,3 +254,42 @@ export function getImportModuleName(
253254
return node.arguments[0].value;
254255
}
255256
}
257+
258+
type AssertNodeInfo = {
259+
matcher: string | null;
260+
isNegated: boolean;
261+
};
262+
/**
263+
* Extracts matcher info from MemberExpression node representing an assert.
264+
*/
265+
export function getAssertNodeInfo(
266+
node: TSESTree.MemberExpression
267+
): AssertNodeInfo {
268+
const emptyInfo = { matcher: null, isNegated: false } as AssertNodeInfo;
269+
270+
if (
271+
!isCallExpression(node.object) ||
272+
!ASTUtils.isIdentifier(node.object.callee)
273+
) {
274+
return emptyInfo;
275+
}
276+
277+
if (node.object.callee.name !== 'expect') {
278+
return emptyInfo;
279+
}
280+
281+
let matcher = ASTUtils.getPropertyName(node);
282+
let isNegated = false;
283+
if (matcher === 'not') {
284+
matcher = isMemberExpression(node.parent)
285+
? ASTUtils.getPropertyName(node.parent)
286+
: null;
287+
isNegated = true;
288+
}
289+
290+
if (!matcher) {
291+
return emptyInfo;
292+
}
293+
294+
return { matcher, isNegated };
295+
}

tests/create-testing-library-rule.test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ ruleTester.run(RULE_NAME, rule, {
121121
`,
122122
},
123123

124+
// Test Cases for presence/absence assertions
125+
// cases: asserts not related to presence/absence
126+
'expect(element).toBeDisabled()',
127+
'expect(element).toBeEnabled()',
128+
129+
// cases: presence/absence matcher not related to assert
130+
'element.toBeInTheDocument()',
131+
'element.not.toBeInTheDocument()',
132+
133+
// cases: weird scenarios to check guard against parent nodes
134+
'expect(element).not()',
135+
'expect(element).not()',
136+
124137
// Test Cases for Queries and Aggressive Queries Reporting
125138
{
126139
code: `
@@ -362,6 +375,36 @@ ruleTester.run(RULE_NAME, rule, {
362375
errors: [{ line: 7, column: 21, messageId: 'fakeError' }],
363376
},
364377

378+
// Test Cases for presence/absence assertions
379+
{
380+
code: `
381+
// case: presence matcher .toBeInTheDocument forced to be reported
382+
expect(element).toBeInTheDocument()
383+
`,
384+
errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }],
385+
},
386+
{
387+
code: `
388+
// case: absence matcher .not.toBeInTheDocument forced to be reported
389+
expect(element).not.toBeInTheDocument()
390+
`,
391+
errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }],
392+
},
393+
{
394+
code: `
395+
// case: presence matcher .not.toBeNull forced to be reported
396+
expect(element).not.toBeNull()
397+
`,
398+
errors: [{ line: 3, column: 7, messageId: 'presenceAssertError' }],
399+
},
400+
{
401+
code: `
402+
// case: absence matcher .toBeNull forced to be reported
403+
expect(element).toBeNull()
404+
`,
405+
errors: [{ line: 3, column: 7, messageId: 'absenceAssertError' }],
406+
},
407+
365408
// Test Cases for Queries and Aggressive Queries Reporting
366409
{
367410
code: `

tests/fake-rule.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { createTestingLibraryRule } from '../lib/create-testing-library-rule';
77

88
export const RULE_NAME = 'fake-rule';
99
type Options = [];
10-
type MessageIds = 'fakeError' | 'getByError' | 'queryByError';
10+
type MessageIds =
11+
| 'fakeError'
12+
| 'getByError'
13+
| 'queryByError'
14+
| 'presenceAssertError'
15+
| 'absenceAssertError';
1116

1217
export default createTestingLibraryRule<Options, MessageIds>({
1318
name: RULE_NAME,
@@ -22,6 +27,8 @@ export default createTestingLibraryRule<Options, MessageIds>({
2227
fakeError: 'fake error reported',
2328
getByError: 'some error related to getBy reported',
2429
queryByError: 'some error related to queryBy reported',
30+
presenceAssertError: 'some error related to presence assert reported',
31+
absenceAssertError: 'some error related to absence assert reported',
2532
},
2633
fixable: null,
2734
schema: [],
@@ -44,7 +51,17 @@ export default createTestingLibraryRule<Options, MessageIds>({
4451
}
4552
};
4653

47-
const checkImportDeclaration = (node: TSESTree.ImportDeclaration) => {
54+
const reportMemberExpression = (node: TSESTree.MemberExpression) => {
55+
if (helpers.isPresenceAssert(node)) {
56+
return context.report({ node, messageId: 'presenceAssertError' });
57+
}
58+
59+
if (helpers.isAbsenceAssert(node)) {
60+
return context.report({ node, messageId: 'absenceAssertError' });
61+
}
62+
};
63+
64+
const reportImportDeclaration = (node: TSESTree.ImportDeclaration) => {
4865
// This is just to check that defining an `ImportDeclaration` doesn't
4966
// override `ImportDeclaration` from `detectTestingLibraryUtils`
5067

@@ -55,7 +72,8 @@ export default createTestingLibraryRule<Options, MessageIds>({
5572

5673
return {
5774
'CallExpression Identifier': reportCallExpressionIdentifier,
58-
ImportDeclaration: checkImportDeclaration,
75+
MemberExpression: reportMemberExpression,
76+
ImportDeclaration: reportImportDeclaration,
5977
'Program:exit'() {
6078
const importNode = helpers.getCustomModuleImportNode();
6179
const importName = helpers.getCustomModuleImportName();

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

-3
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,6 @@ ruleTester.run(RULE_NAME, rule, {
362362
// right after clicking submit button it disappears
363363
expect(submitButton).not.toBeInTheDocument()
364364
`,
365-
// some weird examples after here to check guard against parent nodes
366-
'expect(getByText("button")).not()',
367-
'expect(queryByText("button")).not()',
368365
],
369366
invalid: [
370367
// cases: asserting absence incorrectly with `getBy*` queries

0 commit comments

Comments
 (0)