Skip to content

Commit bb64181

Browse files
committed
refactor: prefer-wait-for with the new settings
1 parent 6018dd1 commit bb64181

File tree

3 files changed

+1883
-146
lines changed

3 files changed

+1883
-146
lines changed

docs/rules/prefer-wait-for.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Deprecated `wait` async utils are:
1717
Examples of **incorrect** code for this rule:
1818

1919
```js
20+
import { wait, waitForElement, waitForDomChange } from '@testing-library/dom';
21+
// this also works for const { wait, waitForElement, waitForDomChange } = require ('@testing-library/dom')
22+
2023
const foo = async () => {
2124
await wait();
2225
await wait(() => {});
@@ -25,18 +28,41 @@ const foo = async () => {
2528
await waitForDomChange(mutationObserverOptions);
2629
await waitForDomChange({ timeout: 100 });
2730
};
31+
32+
import * as tl from '@testing-library/dom';
33+
// this also works for const tl = require('@testing-library/dom')
34+
const foo = async () => {
35+
await tl.wait();
36+
await tl.wait(() => {});
37+
await tl.waitForElement(() => {});
38+
await tl.waitForDomChange();
39+
await tl.waitForDomChange(mutationObserverOptions);
40+
await tl.waitForDomChange({ timeout: 100 });
41+
};
2842
```
2943

3044
Examples of **correct** code for this rule:
3145

3246
```js
47+
import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom';
48+
// this also works for const { waitFor, waitForElementToBeRemoved } = require('@testing-library/dom')
3349
const foo = async () => {
3450
// new waitFor method
3551
await waitFor(() => {});
3652

3753
// previous waitForElementToBeRemoved is not deprecated
3854
await waitForElementToBeRemoved(() => {});
3955
};
56+
57+
import * as tl from '@testing-library/dom';
58+
// this also works for const tl = require('@testing-library/dom')
59+
const foo = async () => {
60+
// new waitFor method
61+
await tl.waitFor(() => {});
62+
63+
// previous waitForElementToBeRemoved is not deprecated
64+
await tl.waitForElementToBeRemoved(() => {});
65+
};
4066
```
4167

4268
## When Not To Use It

lib/rules/prefer-wait-for.ts

Lines changed: 136 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getDocsUrl } from '../utils';
1+
import { TSESTree } from '@typescript-eslint/experimental-utils';
2+
import { createTestingLibraryRule } from '../create-testing-library-rule';
33
import {
44
isImportSpecifier,
55
isMemberExpression,
66
isIdentifier,
77
findClosestCallExpressionNode,
8+
isCallExpression,
9+
isImportDeclaration,
10+
isImportNamespaceSpecifier,
11+
isVariableDeclarator,
12+
isObjectPattern,
13+
isProperty,
814
} from '../node-utils';
915

1016
export const RULE_NAME = 'prefer-wait-for';
11-
export type MessageIds = 'preferWaitForMethod' | 'preferWaitForImport';
17+
export type MessageIds =
18+
| 'preferWaitForMethod'
19+
| 'preferWaitForImport'
20+
| 'preferWaitForRequire';
1221
type Options = [];
1322

1423
const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange'];
1524

16-
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
25+
export default createTestingLibraryRule<Options, MessageIds>({
1726
name: RULE_NAME,
1827
meta: {
1928
type: 'suggestion',
@@ -26,14 +35,43 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
2635
preferWaitForMethod:
2736
'`{{ methodName }}` is deprecated in favour of `waitFor`',
2837
preferWaitForImport: 'import `waitFor` instead of deprecated async utils',
38+
preferWaitForRequire:
39+
'require `waitFor` instead of deprecated async utils',
2940
},
3041

3142
fixable: 'code',
3243
schema: [],
3344
},
3445
defaultOptions: [],
3546

36-
create(context) {
47+
create(context, _, helpers) {
48+
let addWaitFor = false;
49+
50+
const reportRequire = (node: TSESTree.ObjectPattern) => {
51+
context.report({
52+
node: node,
53+
messageId: 'preferWaitForRequire',
54+
fix(fixer) {
55+
const excludedImports = [...DEPRECATED_METHODS, 'waitFor'];
56+
57+
const newAllRequired = node.properties
58+
.filter(
59+
(s) =>
60+
isProperty(s) &&
61+
isIdentifier(s.key) &&
62+
!excludedImports.includes(s.key.name)
63+
)
64+
.map(
65+
(s) => ((s as TSESTree.Property).key as TSESTree.Identifier).name
66+
);
67+
68+
newAllRequired.push('waitFor');
69+
70+
return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`);
71+
},
72+
});
73+
};
74+
3775
const reportImport = (node: TSESTree.ImportDeclaration) => {
3876
context.report({
3977
node: node,
@@ -112,46 +150,100 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
112150
};
113151

114152
return {
115-
'ImportDeclaration[source.value=/testing-library/]'(
116-
node: TSESTree.ImportDeclaration
117-
) {
118-
const deprecatedImportSpecifiers = node.specifiers.filter(
119-
(specifier) =>
120-
isImportSpecifier(specifier) &&
121-
specifier.imported &&
122-
DEPRECATED_METHODS.includes(specifier.imported.name)
123-
);
124-
125-
deprecatedImportSpecifiers.forEach((importSpecifier, i) => {
126-
if (i === 0) {
127-
reportImport(node);
128-
}
129-
130-
context
131-
.getDeclaredVariables(importSpecifier)
132-
.forEach((variable) =>
133-
variable.references.forEach((reference) =>
134-
reportWait(reference.identifier)
135-
)
136-
);
137-
});
153+
'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) {
154+
const isDeprecatedMethod =
155+
isIdentifier(node.property) &&
156+
DEPRECATED_METHODS.includes(node.property.name);
157+
if (!isDeprecatedMethod) {
158+
// the method does not match a deprecated method
159+
return;
160+
}
161+
const testingLibraryNode =
162+
helpers.getCustomModuleImportNode() ??
163+
helpers.getTestingLibraryImportNode();
164+
// this verifies the owner of the MemberExpression is the same as the node if it was imported with "import * as TL from 'foo'"
165+
const callerIsTestingLibraryFromImport =
166+
isIdentifier(node.object) &&
167+
isImportDeclaration(testingLibraryNode) &&
168+
isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) &&
169+
node.object.name === testingLibraryNode.specifiers[0].local.name;
170+
// this verifies the owner of the MemberExpression is the same as the node if it was imported with "const tl = require('foo')"
171+
const callerIsTestingLibraryFromRequire =
172+
isIdentifier(node.object) &&
173+
isCallExpression(testingLibraryNode) &&
174+
isVariableDeclarator(testingLibraryNode.parent) &&
175+
isIdentifier(testingLibraryNode.parent.id) &&
176+
node.object.name === testingLibraryNode.parent.id.name;
177+
if (
178+
!callerIsTestingLibraryFromImport &&
179+
!callerIsTestingLibraryFromRequire
180+
) {
181+
// the method does not match from the imported elements from TL (even from custom)
182+
return;
183+
}
184+
addWaitFor = true;
185+
reportWait(node.property as TSESTree.Identifier); // compiler is not picking up correctly, it should have inferred it is an identifier
138186
},
139-
'ImportDeclaration[source.value=/testing-library/] > ImportNamespaceSpecifier'(
140-
node: TSESTree.ImportNamespaceSpecifier
141-
) {
142-
context.getDeclaredVariables(node).forEach((variable) =>
143-
variable.references.forEach((reference) => {
144-
if (
145-
isMemberExpression(reference.identifier.parent) &&
146-
isIdentifier(reference.identifier.parent.property) &&
147-
DEPRECATED_METHODS.includes(
148-
reference.identifier.parent.property.name
149-
)
150-
) {
151-
reportWait(reference.identifier.parent.property);
152-
}
153-
})
154-
);
187+
'CallExpression > Identifier'(node: TSESTree.Identifier) {
188+
const testingLibraryNode =
189+
helpers.getCustomModuleImportNode() ??
190+
helpers.getTestingLibraryImportNode();
191+
// this verifies the owner of the MemberExpression is the same as the node if it was imported with "import { deprecated as aliased } from 'foo'"
192+
const callerIsTestingLibraryFromImport =
193+
isImportDeclaration(testingLibraryNode) &&
194+
testingLibraryNode.specifiers.some(
195+
(s) =>
196+
isImportSpecifier(s) &&
197+
DEPRECATED_METHODS.includes(s.imported.name) &&
198+
s.local.name === node.name
199+
);
200+
// this verifies the owner of the MemberExpression is the same as the node if it was imported with "const { deprecatedMethod } = require('foo')"
201+
const callerIsTestingLibraryFromRequire =
202+
isCallExpression(testingLibraryNode) &&
203+
isVariableDeclarator(testingLibraryNode.parent) &&
204+
isObjectPattern(testingLibraryNode.parent.id) &&
205+
testingLibraryNode.parent.id.properties.some(
206+
(p) =>
207+
isProperty(p) &&
208+
isIdentifier(p.key) &&
209+
isIdentifier(p.value) &&
210+
p.value.name === node.name &&
211+
DEPRECATED_METHODS.includes(p.key.name)
212+
);
213+
if (
214+
!callerIsTestingLibraryFromRequire &&
215+
!callerIsTestingLibraryFromImport
216+
) {
217+
return;
218+
}
219+
addWaitFor = true;
220+
reportWait(node);
221+
},
222+
'Program:exit'() {
223+
if (!addWaitFor) {
224+
return;
225+
}
226+
// now that all usages of deprecated methods were replaced, remove the extra imports
227+
const testingLibraryNode =
228+
helpers.getCustomModuleImportNode() ??
229+
helpers.getTestingLibraryImportNode();
230+
if (isCallExpression(testingLibraryNode)) {
231+
const parent = testingLibraryNode.parent as TSESTree.VariableDeclarator;
232+
if (!isObjectPattern(parent.id)) {
233+
// if there is no destructuring, there is nothing to replace
234+
return;
235+
}
236+
reportRequire(parent.id);
237+
} else {
238+
if (
239+
testingLibraryNode.specifiers.length === 1 &&
240+
isImportNamespaceSpecifier(testingLibraryNode.specifiers[0])
241+
) {
242+
// if we import everything, there is nothing to replace
243+
return;
244+
}
245+
reportImport(testingLibraryNode);
246+
}
155247
},
156248
};
157249
},

0 commit comments

Comments
 (0)