diff --git a/README.md b/README.md index 56233832..98d06931 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ [![Tweet][tweet-badge]][tweet-url] + [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-) + ## Installation @@ -142,6 +144,7 @@ To enable this configuration use the `extends` property in your | [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | | | [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] | | [no-get-by-for-checking-element-not-present](docs/rules/no-get-by-for-checking-element-not-present) | Disallow the use of `getBy*` queries when checking elements are not present | | | +| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | [build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square @@ -190,6 +193,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/docs/rules/no-manual-cleanup.md b/docs/rules/no-manual-cleanup.md new file mode 100644 index 00000000..cfdd84d8 --- /dev/null +++ b/docs/rules/no-manual-cleanup.md @@ -0,0 +1,41 @@ +# Disallow the use of `cleanup` (no-manual-cleanup) + +`cleanup` is performed automatically if the testing framework you're using supports the `afterEach` global (like mocha, Jest, and Jasmine). In this case, it's unnecessary to do manual cleanups after each test unless you skip the auto-cleanup with environment variables such as `RTL_SKIP_AUTO_CLEANUP` for React. + +## Rule Details + +This rule disallows the import/use of `cleanup` in your test files. It fires if you import `cleanup` from one of these libraries: + +- [Marko Testing Library](https://testing-library.com/docs/marko-testing-library/api#cleanup) +- [Preact Testing Library](https://testing-library.com/docs/preact-testing-library/api#cleanup) +- [React Testing Library](https://testing-library.com/docs/react-testing-library/api#cleanup) +- [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/api#cleanup) +- [Vue Testing Library](https://testing-library.com/docs/vue-testing-library/api#cleanup) + +Examples of **incorrect** code for this rule: + +```js +import { cleanup } from '@testing-library/react'; + +const { cleanup } = require('@testing-library/react'); + +import utils from '@testing-library/react'; +afterEach(() => utils.cleanup()); + +const utils = require('@testing-library/react'); +afterEach(utils.cleanup); +``` + +Examples of **correct** code for this rule: + +```js +import { cleanup } from 'any-other-library'; + +const utils = require('any-other-library'); +utils.cleanup(); +``` + +## Further Reading + +- [cleanup API in React Testing Library](https://testing-library.com/docs/react-testing-library/api#cleanup) +- [Skipping Auto Cleanup](https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup) diff --git a/lib/index.js b/lib/index.js index 58102e71..6b87aed0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,6 +9,7 @@ const rules = { 'no-debug': require('./rules/no-debug'), 'no-dom-import': require('./rules/no-dom-import'), 'no-get-by-for-checking-element-not-present': require('./rules/no-get-by-for-checking-element-not-present'), + 'no-manual-cleanup': require('./rules/no-manual-cleanup'), 'prefer-explicit-assert': require('./rules/prefer-explicit-assert'), }; diff --git a/lib/rules/no-manual-cleanup.js b/lib/rules/no-manual-cleanup.js new file mode 100644 index 00000000..4a53e35a --- /dev/null +++ b/lib/rules/no-manual-cleanup.js @@ -0,0 +1,120 @@ +'use strict'; + +const CLEANUP_LIBRARY_REGEX = /(@testing-library\/(preact|react|svelte|vue))|@marko\/testing-library/; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: ' Disallow the use of `cleanup`', + category: 'Best Practices', + recommended: false, + }, + messages: { + noManualCleanup: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + fixable: null, + schema: [], + }, + + create: function(context) { + let defaultImportFromTestingLibrary; + let defaultRequireFromTestingLibrary; + + return { + ImportDeclaration(node) { + const testingLibraryWithCleanup = node.source.value.match( + CLEANUP_LIBRARY_REGEX + ); + + // Early return if the library doesn't support `cleanup` + if (!testingLibraryWithCleanup) { + return; + } + + if (node.specifiers[0].type === 'ImportDefaultSpecifier') { + defaultImportFromTestingLibrary = node; + } + + const cleanupSpecifier = node.specifiers.find( + specifier => + specifier.imported && specifier.imported.name === 'cleanup' + ); + + if (cleanupSpecifier) { + context.report({ + node: cleanupSpecifier, + messageId: 'noManualCleanup', + }); + } + }, + VariableDeclarator(node) { + if ( + node.init && + node.init.callee && + node.init.callee.name === 'require' + ) { + const requiredModule = node.init.arguments[0]; + const testingLibraryWithCleanup = requiredModule.value.match( + CLEANUP_LIBRARY_REGEX + ); + + // Early return if the library doesn't support `cleanup` + if (!testingLibraryWithCleanup) { + return; + } + + if (node.id.type === 'ObjectPattern') { + const cleanupProperty = node.id.properties.find( + property => property.key.name === 'cleanup' + ); + if (cleanupProperty) { + context.report({ + node: cleanupProperty, + messageId: 'noManualCleanup', + }); + } + } else { + defaultRequireFromTestingLibrary = node.id; + } + } + }, + 'Program:exit'() { + if (defaultImportFromTestingLibrary) { + const references = context.getDeclaredVariables( + defaultImportFromTestingLibrary + )[0].references; + + reportImportReferences(context, references); + } + + if (defaultRequireFromTestingLibrary) { + const references = context + .getDeclaredVariables(defaultRequireFromTestingLibrary.parent)[0] + .references.slice(1); + + reportImportReferences(context, references); + } + }, + }; + }, +}; + +function reportImportReferences(context, references) { + if (references && references.length > 0) { + references.forEach(reference => { + const utilsUsage = reference.identifier.parent; + if ( + utilsUsage && + utilsUsage.property && + utilsUsage.property.name === 'cleanup' + ) { + context.report({ + node: utilsUsage.property, + messageId: 'noManualCleanup', + }); + } + }); + } +} diff --git a/tests/lib/rules/no-manual-cleanup.js b/tests/lib/rules/no-manual-cleanup.js new file mode 100644 index 00000000..e4a9c627 --- /dev/null +++ b/tests/lib/rules/no-manual-cleanup.js @@ -0,0 +1,168 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-manual-cleanup'); +const RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, +}); + +const ALL_TESTING_LIBRARIES_WITH_CLEANUP = [ + '@testing-library/preact', + '@testing-library/react', + '@testing-library/svelte', + '@testing-library/vue', + '@marko/testing-library', +]; + +ruleTester.run('no-manual-cleanup', rule, { + valid: [ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `import { render } from "${lib}"`, + })), + { + code: `import { cleanup } from "any-other-library"`, + }, + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `import utils from "${lib}"`, + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: ` + import utils from "${lib}" + utils.render() + `, + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `const { render, within } = require("${lib}")`, + })), + { + code: `const { cleanup } = require("any-other-library")`, + }, + { + code: ` + const utils = require("any-other-library") + utils.cleanup() + `, + }, + { + // For test coverage + code: `const utils = render("something")`, + }, + ], + invalid: [ + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `import { render, cleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 18, // error points to `cleanup` + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `import { cleanup as myCustomCleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 10, // error points to `cleanup` + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `import utils, { cleanup } from "${lib}"`, + errors: [ + { + line: 1, + column: 17, // error points to `cleanup` + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: ` + import utils from "${lib}" + afterEach(() => utils.cleanup()) + `, + errors: [ + { + line: 3, + column: 31, + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: ` + import utils from "${lib}" + afterEach(utils.cleanup) + `, + errors: [ + { + line: 3, + column: 25, + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: `const { cleanup } = require("${lib}")`, + errors: [ + { + line: 1, + column: 9, // error points to `cleanup` + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: ` + const utils = require("${lib}") + afterEach(() => utils.cleanup()) + `, + errors: [ + { + line: 3, + column: 31, + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ...ALL_TESTING_LIBRARIES_WITH_CLEANUP.map(lib => ({ + code: ` + const utils = require("${lib}") + afterEach(utils.cleanup) + `, + errors: [ + { + line: 3, + column: 25, + message: + "`cleanup` is performed automatically by your test runner, you don't need manual cleanups.", + }, + ], + })), + ], +});