Skip to content

Commit

Permalink
feat: part2 of refactoring user event. improved docs
Browse files Browse the repository at this point in the history
  • Loading branch information
gndelia committed Nov 7, 2020
1 parent 40c3588 commit f12acf5
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 34 deletions.
8 changes: 8 additions & 0 deletions docs/rules/prefer-user-event.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Examples of **incorrect** code for this rule:
```ts
// a method in fireEvent that has a userEvent equivalent
import { fireEvent } from '@testing-library/dom';
// or const { fireEvent } = require('@testing-library/dom');
fireEvent.click(node);

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

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

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

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

// any userEvent method
userEvent.click();

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

import * as dom from '@testing-library/dom';
// or const dom = require('@testing-library/dom');
dom.fireEvent.cut(node);
```

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

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

// using fireEvent after importing the entire library
import * as dom from '@testing-library/dom';
// or const dom = require('@testing-library/dom');
dom.fireEvent.click(node);
dom.fireEvent.change(node, { target: { value: 'foo' } });
```
Expand Down
55 changes: 53 additions & 2 deletions lib/detect-testing-library-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
import { getImportModuleName, isLiteral, ImportModuleNode } from './node-utils';
import {
getImportModuleName,
isLiteral,
ImportModuleNode,
isImportDeclaration,
isImportNamespaceSpecifier,
isImportSpecifier,
isIdentifier,
isProperty,
} from './node-utils';

export type TestingLibrarySettings = {
'testing-library/module'?: string;
Expand Down Expand Up @@ -33,6 +42,9 @@ export type DetectionHelpers = {
getIsTestingLibraryImported: () => boolean;
getIsValidFilename: () => boolean;
canReportErrors: () => boolean;
findImportedUtilSpecifier: (
specifierName: string
) => TSESTree.ImportClause | TSESTree.Identifier | undefined;
};

const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$';
Expand Down Expand Up @@ -106,7 +118,46 @@ export function detectTestingLibraryUtils<
* Wraps all conditions that must be met to report rules.
*/
canReportErrors() {
return this.getIsTestingLibraryImported() && this.getIsValidFilename();
return (
helpers.getIsTestingLibraryImported() && helpers.getIsValidFilename()
);
},
/**
* Gets a string and verifies if it was imported/required by our custom module node
*/
findImportedUtilSpecifier(specifierName: string) {
const node =
helpers.getCustomModuleImportNode() ??
helpers.getTestingLibraryImportNode();
if (!node) {
return null;
}
if (isImportDeclaration(node)) {
const namedExport = node.specifiers.find(
(n) => isImportSpecifier(n) && n.imported.name === specifierName
);
// it is "import { foo [as alias] } from 'baz'""
if (namedExport) {
return namedExport;
}
// it could be "import * as rtl from 'baz'"
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
} else {
const requireNode = node.parent as TSESTree.VariableDeclarator;
if (isIdentifier(requireNode.id)) {
// this is const rtl = require('foo')
return requireNode.id;
}
// this should be const { something } = require('foo')
const destructuring = requireNode.id as TSESTree.ObjectPattern;
const property = destructuring.properties.find(
(n) =>
isProperty(n) &&
isIdentifier(n.key) &&
n.key.name === specifierName
);
return (property as TSESTree.Property).key as TSESTree.Identifier;
}
},
};

Expand Down
19 changes: 0 additions & 19 deletions lib/node-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,22 +253,3 @@ export function getImportModuleName(
return node.arguments[0].value;
}
}

export function getSpecifierFromImport(
node: ImportModuleNode,
specifierName: string
) {
if (isImportDeclaration(node)) {
const namedExport = node.specifiers.find(
(node) => isImportSpecifier(node) && node.imported.name === specifierName
);
// it is "import { foo } from 'baz'""
if (namedExport) {
return namedExport;
}
// it could be "import * as rtl from 'baz'"
return node.specifiers.find((n) => isImportNamespaceSpecifier(n));
} else {
// TODO make it work for require
}
}
18 changes: 5 additions & 13 deletions lib/rules/prefer-user-event.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { TSESTree } from '@typescript-eslint/experimental-utils';
import { createTestingLibraryRule } from '../create-testing-library-rule';
import {
isIdentifier,
isMemberExpression,
getSpecifierFromImport,
} from '../node-utils';
import { isIdentifier, isMemberExpression } from '../node-utils';

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

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

return {
['CallExpression > MemberExpression'](node: TSESTree.MemberExpression) {
if (!helpers.getIsTestingLibraryImported()) {
return;
}
const testingLibraryImportNode = helpers.getTestingLibraryImportNode();
const fireEventAliasOrWildcard = getSpecifierFromImport(
testingLibraryImportNode,
'fireEvent'
)?.local.name;
const util = helpers.findImportedUtilSpecifier('fireEvent');
const fireEventAliasOrWildcard = isIdentifier(util)
? util?.name
: util?.local.name;

if (!fireEventAliasOrWildcard) {
// testing library was imported, but fireEvent was not imported
Expand Down
109 changes: 109 additions & 0 deletions tests/lib/rules/prefer-user-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,67 @@ ruleTester.run(RULE_NAME, rule, {
fireEvent()
`,
})),
{
settings: {
'testing-library/module': 'test-utils',
},
code: `
import { screen } from 'test-utils'
const element = screen.getByText(foo)
`,
},
{
settings: {
'testing-library/module': 'test-utils',
},
code: `
import { render } from 'test-utils'
const utils = render(baz)
const element = utils.getByText(foo)
`,
},
...UserEventMethods.map((userEventMethod) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import userEvent from 'test-utils'
const node = document.createElement(elementType)
userEvent.${userEventMethod}(foo)
`,
})),
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import { fireEvent } from 'test-utils'
const node = document.createElement(elementType)
fireEvent.${fireEventMethod}(foo)
`,
options: [{ allowedMethods: [fireEventMethod] }],
})),
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import { fireEvent as fireEventAliased } from 'test-utils'
const node = document.createElement(elementType)
fireEventAliased.${fireEventMethod}(foo)
`,
options: [{ allowedMethods: [fireEventMethod] }],
})),
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import * as dom from 'test-utils'
dom.fireEvent.${fireEventMethod}(foo)
`,
options: [{ allowedMethods: [fireEventMethod] }],
})),
],
invalid: [
...createScenarioWithImport<InvalidTestCase<MessageIds, Options>>(
Expand All @@ -130,5 +191,53 @@ ruleTester.run(RULE_NAME, rule, {
errors: [{ messageId: 'preferUserEvent' }],
})
),
...createScenarioWithImport<InvalidTestCase<MessageIds, Options>>(
(libraryModule: string, fireEventMethod: string) => ({
code: `
const { fireEvent } = require('${libraryModule}')
fireEvent.${fireEventMethod}(foo)
`,
errors: [{ messageId: 'preferUserEvent' }],
})
),
...createScenarioWithImport<InvalidTestCase<MessageIds, Options>>(
(libraryModule: string, fireEventMethod: string) => ({
code: `
const rtl = require('${libraryModule}')
rtl.fireEvent.${fireEventMethod}(foo)
`,
errors: [{ messageId: 'preferUserEvent' }],
})
),
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import * as dom from 'test-utils'
dom.fireEvent.${fireEventMethod}(foo)
`,
errors: [{ messageId: 'preferUserEvent' }],
})),
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import { fireEvent } from 'test-utils'
fireEvent.${fireEventMethod}(foo)
`,
errors: [{ messageId: 'preferUserEvent' }],
})),
...Object.keys(MappingToUserEvent).map((fireEventMethod: string) => ({
settings: {
'testing-library/module': 'test-utils',
},
code: `
import { fireEvent as fireEventAliased } from 'test-utils'
fireEventAliased.${fireEventMethod}(foo)
`,
errors: [{ messageId: 'preferUserEvent' }],
})),
],
});

0 comments on commit f12acf5

Please sign in to comment.