Skip to content

Feature/rule no dom import #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ To enable this configuration use the `extends` property in your
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended][] | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | | ![angular][] ![react][] ![vue][] |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | | ![angular][] ![react][] ![vue][] |

[recommended]: https://img.shields.io/badge/recommended-lightgrey?style=flat-square
[angular]: https://img.shields.io/badge/-Angular-black?style=flat-square&logo=angular&logoColor=white&labelColor=DD0031&color=black
Expand Down
64 changes: 64 additions & 0 deletions docs/rules/no-dom-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Disallow importing from DOM Testing Library

Ensure that there are no direct imports from `@testing-library/dom` or
`@testing-library/dom` when using some testing library framework
wrapper.

## Rule Details

Testing Library framework wrappers as React Testing Library already
re-exports everything from DOM Testing Library so you always have to
import DOM Testing Library utils from corresponding framework wrapper
module to:

- use proper extended version of some of those methods containing
additional functionality related to specific framework (e.g.
`fireEvent` util)
- avoid importing from extraneous dependencies (similar to
eslint-plugin-import)

This rule aims to prevent users from import anything directly from
`@testing-library/dom` (or `dom-testing-library`) and it's useful for
new starters or when IDEs autoimport from wrong module.

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

```js
import { fireEvent } from 'dom-testing-library';
```

```js
import { fireEvent } from '@testing-library/dom';
```

```js
const { fireEvent } = require('dom-testing-library');
```

```js
const { fireEvent } = require('@testing-library/dom');
```

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

```js
import { fireEvent } from 'react-testing-library';
```

```js
import { fireEvent } from '@testing-library/react';
```

```js
const { fireEvent } = require('react-testing-library');
```

```js
const { fireEvent } = require('@testing-library/react');
```

## Further Reading

- [Angular Testing Library API](https://testing-library.com/docs/angular-testing-library/api)
- [React Testing Library API](https://testing-library.com/docs/react-testing-library/api)
- [Vue Testing Library API](https://testing-library.com/docs/vue-testing-library/api)
52 changes: 52 additions & 0 deletions lib/rules/no-dom-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';

const { getDocsUrl } = require('../utils');

const DOM_TESTING_LIBRARY_MODULES = [
'dom-testing-library',
'@testing-library/dom',
];

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow importing from DOM Testing Library',
category: 'Best Practices',
recommended: false,
url: getDocsUrl('no-dom-import'),
},
messages: {
noDomImport:
'import from DOM Testing Library is restricted, import from corresponding Testing library framework instead',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message sounds unclear to me. What do you think?

Copy link
Member Author

@Belco90 Belco90 Sep 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not convinced at all about it but I can't suggest which module should be imported. What I can do is to check which DTL module is used (dom-testing-library or @testing-library/dom) and say something like import directly from '@testing-library/dom' is restricted, instead you must import from '@testing-library/<framework>' by literally leaving that <framework> placeholder

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would think of something like that too. I see two solutions possible to that:

  1. For each rule, we could configure it by adding the selected framework and then use it in the proper rule. For example:
{
  "rules": {
    "testing-library/no-dom-import": ["error", "react"]
  }
}

However, this would require the user to add the framework to each rule where we need to differentiate the frameworks.

  1. Generate a set of rules for every corresponding framework in this file. This could become:
module.exports = {
  rules,
  configs: {
    recommended: {
      plugins: ['testing-library'],
      rules: recommendedRules,
    },
    react: {
      plugins: ['testing-library'],
      rules: { ...generateRecommendedRules("react"), 'testing-library/no-debug': 'warn' },
    },
    // ...
  },
}

And in generateRecommendedRules:

function generateRecommendedRules(framework) {
  return allRules.map(rule => {
    const augmentedRule = rule(framework)
    return augmentedRule
  })
}

Where rule would be a higher-order function that returns the proper rule configuration based on the framework:

'use strict';

const { getDocsUrl } = require('../utils');

const DOM_TESTING_LIBRARY_MODULES = [
  'dom-testing-library',
  '@testing-library/dom',
];

module.exports = (framework) => {
  return {
    meta: {
      type: 'problem',
      docs: {
        description: 'Disallow importing from DOM Testing Library',
        // ...
     },
    messages: {
      noDomImport:
        `import from DOM Testing Library is restricted, import from "@testing-library/${framework}`,
    },
    fixable: null,
    schema: [],
  },
  // ...
}

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️ of course, didn't think about context options! Really interesting approach, I'd go for something intermediate between those 2 options: we add the framework option to this rule through context.options (so you receive it within create function through context arg and pass it to report message, as this seems the standard instead of doing the whole workaround to pass it to the rule function itself) and then we setup automatically this option on each recommended framework config (so it's automatically setup for those frameworks but everyone can customize it when adding the rule manually).

So I'm gonna merge this one (as I wanted to improve the error message but obviously it's gonna be way better when the rule options are received so I'm gonna skip that for now) and create an issue for you to handle that separately, but only for receiving the context.options and improve the message with proper framework setup, nothing yet about fixable. Are you happy with that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brilliant!

},
fixable: null,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be awesome (and I think pretty easy though I'm not sure of that) to include a fix for this rule. I can look into it if you want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should, and I was thinking about it but I prefer to leave the fixable for a future version. Every time I think about this one it seems more complex to implement as you don't know which TL framework the query must be imported (don't think about react only, this works for vue and angular). So for fixing the rule we could try to guess from where the user should import it by looking for other "@testing-library/" text and get that suffix to replace it by "@testing-library/dom" but then... what if it's the first import in the file referring to testing library? We can't guess from where we have to reimport. Did you have something else in mind for this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes that's true. This is the same problem than above, I have two potential solutions 👆

schema: [],
},

create: function(context) {
return {
ImportDeclaration(node) {
if (DOM_TESTING_LIBRARY_MODULES.includes(node.source.value)) {
context.report({
node,
messageId: 'noDomImport',
});
}
},

[`CallExpression > Identifier[name="require"]`](node) {
const { arguments: args } = node.parent;

if (
args.some(args => DOM_TESTING_LIBRARY_MODULES.includes(args.value))
) {
context.report({
node,
messageId: 'noDomImport',
});
}
},
};
},
};
111 changes: 111 additions & 0 deletions tests/lib/rules/no-dom-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict';

const rule = require('../../../lib/rules/no-dom-import');
const RuleTester = require('eslint').RuleTester;

const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2015, sourceType: 'module' },
});

ruleTester.run('no-dom-import', rule, {
valid: [
{ code: 'import { foo } from "foo"' },
{ code: 'import "foo"' },
{ code: 'import { fireEvent } from "react-testing-library"' },
{ code: 'import * as testing from "react-testing-library"' },
{ code: 'import { fireEvent } from "@testing-library/react"' },
{ code: 'import * as testing from "@testing-library/react"' },
{ code: 'import "react-testing-library"' },
{ code: 'import "@testing-library/react"' },
{ code: 'const { foo } = require("foo")' },
{ code: 'require("foo")' },
{ code: 'require("")' },
{ code: 'require()' },
{ code: 'const { fireEvent } = require("react-testing-library")' },
{ code: 'const { fireEvent } = require("@testing-library/react")' },
{ code: 'require("react-testing-library")' },
{ code: 'require("@testing-library/react")' },
],
invalid: [
{
code: 'import { fireEvent } from "dom-testing-library"',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'import * as testing from "dom-testing-library"',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'import { fireEvent } from "@testing-library/dom"',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'import * as testing from "@testing-library/dom"',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'import "dom-testing-library"',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'import "@testing-library/dom"',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'const { fireEvent } = require("dom-testing-library")',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'const { fireEvent } = require("@testing-library/dom")',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'require("dom-testing-library")',
errors: [
{
messageId: 'noDomImport',
},
],
},
{
code: 'require("@testing-library/dom")',
errors: [
{
messageId: 'noDomImport',
},
],
},
],
});