diff --git a/README.md b/README.md index f1ee11f6..350ec79b 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ To enable this configuration use the `extends` property in your | [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | | | [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | | | [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | | +| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] | [build-badge]: https://img.shields.io/travis/testing-library/eslint-plugin-testing-library?style=flat-square [build-url]: https://travis-ci.org/testing-library/eslint-plugin-testing-library diff --git a/docs/rules/prefer-wait-for.md b/docs/rules/prefer-wait-for.md new file mode 100644 index 00000000..a284c32a --- /dev/null +++ b/docs/rules/prefer-wait-for.md @@ -0,0 +1,48 @@ +# Use `waitFor` instead of deprecated wait methods (prefer-wait-for) + +`dom-testing-library` v7 released a new async util called `waitFor` which satisfies the use cases of `wait`, `waitForElement`, and `waitForDomChange` making them deprecated. + +## Rule Details + +This rule aims to use `waitFor` async util rather than previous deprecated ones. + +Deprecated `wait` async utils are: + +- `wait` +- `waitForElement` +- `waitForDomChange` + +> This rule will auto fix deprecated async utils for you, including the necessary empty callback for `waitFor`. This means `wait();` will be replaced with `waitFor(() => {});` + +Examples of **incorrect** code for this rule: + +```js +const foo = async () => { + await wait(); + await wait(() => {}); + await waitForElement(() => {}); + await waitForDomChange(); + await waitForDomChange(mutationObserverOptions); + await waitForDomChange({ timeout: 100}); +}; +``` + +Examples of **correct** code for this rule: + +```js +const foo = async () => { + // new waitFor method + await waitFor(() => {}); + + // previous waitForElementToBeRemoved is not deprecated + await waitForElementToBeRemoved(() => {}); +}; +``` + +## When Not To Use It + +When using dom-testing-library (or any other Testing Library relying on dom-testing-library) prior to v7. + +## Further Reading + +- [dom-testing-library v7 release](https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0) diff --git a/lib/index.js b/lib/index.js index 9def7b73..4589eb2c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -12,6 +12,7 @@ const rules = { 'no-manual-cleanup': require('./rules/no-manual-cleanup'), 'no-wait-for-empty-callback': require('./rules/no-wait-for-empty-callback'), 'prefer-explicit-assert': require('./rules/prefer-explicit-assert'), + 'prefer-wait-for': require('./rules/prefer-wait-for'), }; const recommendedRules = { diff --git a/lib/rules/prefer-wait-for.js b/lib/rules/prefer-wait-for.js new file mode 100644 index 00000000..0313922c --- /dev/null +++ b/lib/rules/prefer-wait-for.js @@ -0,0 +1,120 @@ +'use strict'; + +const { getDocsUrl } = require('../utils'); + +const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange']; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Use `waitFor` instead of deprecated wait methods', + category: 'Best Practices', + recommended: false, + url: getDocsUrl('prefer-wait-for'), + }, + messages: { + preferWaitForMethod: + '`{{ methodName }}` is deprecated in favour of `waitFor`', + preferWaitForImport: 'import `waitFor` instead of deprecated async utils', + }, + fixable: 'code', + schema: [], + }, + + create: function(context) { + const importNodes = []; + const waitNodes = []; + + const reportImport = node => { + context.report({ + node: node, + messageId: 'preferWaitForImport', + fix(fixer) { + const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; + + // get all import names excluding all testing library `wait*` utils... + const newImports = node.specifiers + .filter( + specifier => !excludedImports.includes(specifier.imported.name) + ) + .map(specifier => specifier.imported.name); + + // ... and append `waitFor` + newImports.push('waitFor'); + + // build new node with new imports and previous source value + const newNode = `import { ${newImports.join(',')} } from '${ + node.source.value + }';`; + + return fixer.replaceText(node, newNode); + }, + }); + }; + + const reportWait = node => { + context.report({ + node: node, + messageId: 'preferWaitForMethod', + data: { + methodName: node.name, + }, + fix(fixer) { + const { parent } = node; + const [arg] = parent.arguments; + const fixers = []; + + if (arg) { + // if method been fixed already had a callback + // then we just replace the method name. + fixers.push(fixer.replaceText(node, 'waitFor')); + + if (node.name === 'waitForDomChange') { + // if method been fixed is `waitForDomChange` + // then the arg received was options object so we need to insert + // empty callback before. + fixers.push(fixer.insertTextBefore(arg, `() => {}, `)); + } + } else { + // if wait method been fixed didn't have any callback + // then we replace the method name and include an empty callback. + fixers.push(fixer.replaceText(parent, 'waitFor(() => {})')); + } + + return fixers; + }, + }); + }; + + return { + 'ImportDeclaration[source.value=/testing-library/]'(node) { + const importedNames = node.specifiers + .map(specifier => specifier.imported && specifier.imported.name) + .filter(Boolean); + + if ( + importedNames.some(importedName => + DEPRECATED_METHODS.includes(importedName) + ) + ) { + importNodes.push(node); + } + }, + 'CallExpression > Identifier[name=/^(wait|waitForElement|waitForDomChange)$/]'( + node + ) { + waitNodes.push(node); + }, + 'Program:exit'() { + waitNodes.forEach(waitNode => { + reportWait(waitNode); + }); + + importNodes.forEach(importNode => { + reportImport(importNode); + }); + }, + }; + }, +}; diff --git a/tests/lib/rules/prefer-wait-for.js b/tests/lib/rules/prefer-wait-for.js new file mode 100644 index 00000000..85212e01 --- /dev/null +++ b/tests/lib/rules/prefer-wait-for.js @@ -0,0 +1,452 @@ +'use strict'; + +const rule = require('../../../lib/rules/prefer-wait-for'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2018, sourceType: 'module' }, +}); +ruleTester.run('prefer-wait-for', rule, { + valid: [ + { + code: `import { waitFor, render } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + code: `import { waitForElementToBeRemoved, render } from '@testing-library/foo'; + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + }, + { + code: `import { render } from '@testing-library/foo'; + import { waitForSomethingElse } from 'other-module'; + + async () => { + await waitForSomethingElse(() => {}); + }`, + }, + ], + + invalid: [ + { + code: `import { wait, render } from '@testing-library/foo'; + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + // this import doesn't have trailing semicolon but fixer adds it + code: `import { render, wait } from '@testing-library/foo' + + async () => { + await wait(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + code: `import { render, wait, screen } from "@testing-library/foo"; + + async () => { + await wait(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + { + code: `import { render, waitForElement, screen } from '@testing-library/foo' + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + code: `import { waitForElement } from '@testing-library/foo'; + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + { + code: `import { waitForDomChange } from '@testing-library/foo'; + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + code: `import { waitForDomChange } from '@testing-library/foo'; + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + }, + { + code: `import { waitForDomChange } from '@testing-library/foo'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + }, + { + code: `import { waitForDomChange, wait, waitForElement } from '@testing-library/foo'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 8, + column: 15, + }, + ], + output: `import { waitFor } from '@testing-library/foo'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + { + code: `import { render, waitForDomChange, wait, waitForElement } from '@testing-library/foo'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `import { render,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + { + code: `import { waitForDomChange, wait, render, waitForElement } from '@testing-library/foo'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, + ], + output: `import { render,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + { + code: `import { + waitForDomChange, + wait, + render, + waitForElement, + } from '@testing-library/foo'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 9, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 10, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 11, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 12, + column: 15, + }, + ], + output: `import { render,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + { + // if already importing waitFor then it's not imported twice + code: `import { wait, waitFor, render } from '@testing-library/foo'; + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '@testing-library/foo'; + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + }, + ], +});