Skip to content

Commit f12acf5

Browse files
committed
feat: part2 of refactoring user event. improved docs
1 parent 40c3588 commit f12acf5

File tree

5 files changed

+175
-34
lines changed

5 files changed

+175
-34
lines changed

docs/rules/prefer-user-event.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Examples of **incorrect** code for this rule:
1818
```ts
1919
// a method in fireEvent that has a userEvent equivalent
2020
import { fireEvent } from '@testing-library/dom';
21+
// or const { fireEvent } = require('@testing-library/dom');
2122
fireEvent.click(node);
2223

2324
// using fireEvent with an alias
@@ -26,21 +27,26 @@ fireEventAliased.click(node);
2627

2728
// using fireEvent after importing the entire library
2829
import * as dom from '@testing-library/dom';
30+
// or const dom = require(@testing-library/dom');
2931
dom.fireEvent.click(node);
3032
```
3133

3234
Examples of **correct** code for this rule:
3335

3436
```ts
3537
import userEvent from '@testing-library/user-event';
38+
// or const userEvent = require('@testing-library/user-event');
3639

3740
// any userEvent method
3841
userEvent.click();
3942

4043
// fireEvent method that does not have an alternative in userEvent
44+
import { fireEvent } from '@testing-library/dom';
45+
// or const { fireEvent } = require('@testing-library/dom');
4146
fireEvent.cut(node);
4247

4348
import * as dom from '@testing-library/dom';
49+
// or const dom = require('@testing-library/dom');
4450
dom.fireEvent.cut(node);
4551
```
4652

@@ -69,6 +75,7 @@ With this configuration example, the following use cases are considered valid
6975
```ts
7076
// using a named import
7177
import { fireEvent } from '@testing-library/dom';
78+
// or const { fireEvent } = require('@testing-library/dom');
7279
fireEvent.click(node);
7380
fireEvent.change(node, { target: { value: 'foo' } });
7481

@@ -79,6 +86,7 @@ fireEventAliased.change(node, { target: { value: 'foo' } });
7986

8087
// using fireEvent after importing the entire library
8188
import * as dom from '@testing-library/dom';
89+
// or const dom = require('@testing-library/dom');
8290
dom.fireEvent.click(node);
8391
dom.fireEvent.change(node, { target: { value: 'foo' } });
8492
```

lib/detect-testing-library-utils.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
2-
import { getImportModuleName, isLiteral, ImportModuleNode } from './node-utils';
2+
import {
3+
getImportModuleName,
4+
isLiteral,
5+
ImportModuleNode,
6+
isImportDeclaration,
7+
isImportNamespaceSpecifier,
8+
isImportSpecifier,
9+
isIdentifier,
10+
isProperty,
11+
} from './node-utils';
312

413
export type TestingLibrarySettings = {
514
'testing-library/module'?: string;
@@ -33,6 +42,9 @@ export type DetectionHelpers = {
3342
getIsTestingLibraryImported: () => boolean;
3443
getIsValidFilename: () => boolean;
3544
canReportErrors: () => boolean;
45+
findImportedUtilSpecifier: (
46+
specifierName: string
47+
) => TSESTree.ImportClause | TSESTree.Identifier | undefined;
3648
};
3749

3850
const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$';
@@ -106,7 +118,46 @@ export function detectTestingLibraryUtils<
106118
* Wraps all conditions that must be met to report rules.
107119
*/
108120
canReportErrors() {
109-
return this.getIsTestingLibraryImported() && this.getIsValidFilename();
121+
return (
122+
helpers.getIsTestingLibraryImported() && helpers.getIsValidFilename()
123+
);
124+
},
125+
/**
126+
* Gets a string and verifies if it was imported/required by our custom module node
127+
*/
128+
findImportedUtilSpecifier(specifierName: string) {
129+
const node =
130+
helpers.getCustomModuleImportNode() ??
131+
helpers.getTestingLibraryImportNode();
132+
if (!node) {
133+
return null;
134+
}
135+
if (isImportDeclaration(node)) {
136+
const namedExport = node.specifiers.find(
137+
(n) => isImportSpecifier(n) && n.imported.name === specifierName
138+
);
139+
// it is "import { foo [as alias] } from 'baz'""
140+
if (namedExport) {
141+
return namedExport;
142+
}
143+
// it could be "import * as rtl from 'baz'"
144+
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
145+
} else {
146+
const requireNode = node.parent as TSESTree.VariableDeclarator;
147+
if (isIdentifier(requireNode.id)) {
148+
// this is const rtl = require('foo')
149+
return requireNode.id;
150+
}
151+
// this should be const { something } = require('foo')
152+
const destructuring = requireNode.id as TSESTree.ObjectPattern;
153+
const property = destructuring.properties.find(
154+
(n) =>
155+
isProperty(n) &&
156+
isIdentifier(n.key) &&
157+
n.key.name === specifierName
158+
);
159+
return (property as TSESTree.Property).key as TSESTree.Identifier;
160+
}
110161
},
111162
};
112163

lib/node-utils.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -253,22 +253,3 @@ export function getImportModuleName(
253253
return node.arguments[0].value;
254254
}
255255
}
256-
257-
export function getSpecifierFromImport(
258-
node: ImportModuleNode,
259-
specifierName: string
260-
) {
261-
if (isImportDeclaration(node)) {
262-
const namedExport = node.specifiers.find(
263-
(node) => isImportSpecifier(node) && node.imported.name === specifierName
264-
);
265-
// it is "import { foo } from 'baz'""
266-
if (namedExport) {
267-
return namedExport;
268-
}
269-
// it could be "import * as rtl from 'baz'"
270-
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
271-
} else {
272-
// TODO make it work for require
273-
}
274-
}

lib/rules/prefer-user-event.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { TSESTree } from '@typescript-eslint/experimental-utils';
22
import { createTestingLibraryRule } from '../create-testing-library-rule';
3-
import {
4-
isIdentifier,
5-
isMemberExpression,
6-
getSpecifierFromImport,
7-
} from '../node-utils';
3+
import { isIdentifier, isMemberExpression } from '../node-utils';
84

95
export const RULE_NAME = 'prefer-user-event';
106

@@ -96,14 +92,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
9692

9793
return {
9894
['CallExpression > MemberExpression'](node: TSESTree.MemberExpression) {
99-
if (!helpers.getIsTestingLibraryImported()) {
100-
return;
101-
}
102-
const testingLibraryImportNode = helpers.getTestingLibraryImportNode();
103-
const fireEventAliasOrWildcard = getSpecifierFromImport(
104-
testingLibraryImportNode,
105-
'fireEvent'
106-
)?.local.name;
95+
const util = helpers.findImportedUtilSpecifier('fireEvent');
96+
const fireEventAliasOrWildcard = isIdentifier(util)
97+
? util?.name
98+
: util?.local.name;
10799

108100
if (!fireEventAliasOrWildcard) {
109101
// testing library was imported, but fireEvent was not imported

tests/lib/rules/prefer-user-event.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,67 @@ ruleTester.run(RULE_NAME, rule, {
105105
fireEvent()
106106
`,
107107
})),
108+
{
109+
settings: {
110+
'testing-library/module': 'test-utils',
111+
},
112+
code: `
113+
import { screen } from 'test-utils'
114+
const element = screen.getByText(foo)
115+
`,
116+
},
117+
{
118+
settings: {
119+
'testing-library/module': 'test-utils',
120+
},
121+
code: `
122+
import { render } from 'test-utils'
123+
const utils = render(baz)
124+
const element = utils.getByText(foo)
125+
`,
126+
},
127+
...UserEventMethods.map((userEventMethod) => ({
128+
settings: {
129+
'testing-library/module': 'test-utils',
130+
},
131+
code: `
132+
import userEvent from 'test-utils'
133+
const node = document.createElement(elementType)
134+
userEvent.${userEventMethod}(foo)
135+
`,
136+
})),
137+
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
138+
settings: {
139+
'testing-library/module': 'test-utils',
140+
},
141+
code: `
142+
import { fireEvent } from 'test-utils'
143+
const node = document.createElement(elementType)
144+
fireEvent.${fireEventMethod}(foo)
145+
`,
146+
options: [{ allowedMethods: [fireEventMethod] }],
147+
})),
148+
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
149+
settings: {
150+
'testing-library/module': 'test-utils',
151+
},
152+
code: `
153+
import { fireEvent as fireEventAliased } from 'test-utils'
154+
const node = document.createElement(elementType)
155+
fireEventAliased.${fireEventMethod}(foo)
156+
`,
157+
options: [{ allowedMethods: [fireEventMethod] }],
158+
})),
159+
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
160+
settings: {
161+
'testing-library/module': 'test-utils',
162+
},
163+
code: `
164+
import * as dom from 'test-utils'
165+
dom.fireEvent.${fireEventMethod}(foo)
166+
`,
167+
options: [{ allowedMethods: [fireEventMethod] }],
168+
})),
108169
],
109170
invalid: [
110171
...createScenarioWithImport<InvalidTestCase<MessageIds, Options>>(
@@ -130,5 +191,53 @@ ruleTester.run(RULE_NAME, rule, {
130191
errors: [{ messageId: 'preferUserEvent' }],
131192
})
132193
),
194+
...createScenarioWithImport<InvalidTestCase<MessageIds, Options>>(
195+
(libraryModule: string, fireEventMethod: string) => ({
196+
code: `
197+
const { fireEvent } = require('${libraryModule}')
198+
fireEvent.${fireEventMethod}(foo)
199+
`,
200+
errors: [{ messageId: 'preferUserEvent' }],
201+
})
202+
),
203+
...createScenarioWithImport<InvalidTestCase<MessageIds, Options>>(
204+
(libraryModule: string, fireEventMethod: string) => ({
205+
code: `
206+
const rtl = require('${libraryModule}')
207+
rtl.fireEvent.${fireEventMethod}(foo)
208+
`,
209+
errors: [{ messageId: 'preferUserEvent' }],
210+
})
211+
),
212+
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
213+
settings: {
214+
'testing-library/module': 'test-utils',
215+
},
216+
code: `
217+
import * as dom from 'test-utils'
218+
dom.fireEvent.${fireEventMethod}(foo)
219+
`,
220+
errors: [{ messageId: 'preferUserEvent' }],
221+
})),
222+
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
223+
settings: {
224+
'testing-library/module': 'test-utils',
225+
},
226+
code: `
227+
import { fireEvent } from 'test-utils'
228+
fireEvent.${fireEventMethod}(foo)
229+
`,
230+
errors: [{ messageId: 'preferUserEvent' }],
231+
})),
232+
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
233+
settings: {
234+
'testing-library/module': 'test-utils',
235+
},
236+
code: `
237+
import { fireEvent as fireEventAliased } from 'test-utils'
238+
fireEventAliased.${fireEventMethod}(foo)
239+
`,
240+
errors: [{ messageId: 'preferUserEvent' }],
241+
})),
133242
],
134243
});

0 commit comments

Comments
 (0)