From bb6418122bf97c7d2070b5712033546d88774cbb Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Sat, 14 Nov 2020 20:02:54 -0300 Subject: [PATCH 1/4] refactor: prefer-wait-for with the new settings --- docs/rules/prefer-wait-for.md | 26 + lib/rules/prefer-wait-for.ts | 180 ++- tests/lib/rules/prefer-wait-for.test.ts | 1823 +++++++++++++++++++++-- 3 files changed, 1883 insertions(+), 146 deletions(-) diff --git a/docs/rules/prefer-wait-for.md b/docs/rules/prefer-wait-for.md index ac32c7a5..cf607e72 100644 --- a/docs/rules/prefer-wait-for.md +++ b/docs/rules/prefer-wait-for.md @@ -17,6 +17,9 @@ Deprecated `wait` async utils are: Examples of **incorrect** code for this rule: ```js +import { wait, waitForElement, waitForDomChange } from '@testing-library/dom'; +// this also works for const { wait, waitForElement, waitForDomChange } = require ('@testing-library/dom') + const foo = async () => { await wait(); await wait(() => {}); @@ -25,11 +28,24 @@ const foo = async () => { await waitForDomChange(mutationObserverOptions); await waitForDomChange({ timeout: 100 }); }; + +import * as tl from '@testing-library/dom'; +// this also works for const tl = require('@testing-library/dom') +const foo = async () => { + await tl.wait(); + await tl.wait(() => {}); + await tl.waitForElement(() => {}); + await tl.waitForDomChange(); + await tl.waitForDomChange(mutationObserverOptions); + await tl.waitForDomChange({ timeout: 100 }); +}; ``` Examples of **correct** code for this rule: ```js +import { waitFor, waitForElementToBeRemoved } from '@testing-library/dom'; +// this also works for const { waitFor, waitForElementToBeRemoved } = require('@testing-library/dom') const foo = async () => { // new waitFor method await waitFor(() => {}); @@ -37,6 +53,16 @@ const foo = async () => { // previous waitForElementToBeRemoved is not deprecated await waitForElementToBeRemoved(() => {}); }; + +import * as tl from '@testing-library/dom'; +// this also works for const tl = require('@testing-library/dom') +const foo = async () => { + // new waitFor method + await tl.waitFor(() => {}); + + // previous waitForElementToBeRemoved is not deprecated + await tl.waitForElementToBeRemoved(() => {}); +}; ``` ## When Not To Use It diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index 00d1fd5a..6c61ca80 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -1,19 +1,28 @@ -import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; -import { getDocsUrl } from '../utils'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { createTestingLibraryRule } from '../create-testing-library-rule'; import { isImportSpecifier, isMemberExpression, isIdentifier, findClosestCallExpressionNode, + isCallExpression, + isImportDeclaration, + isImportNamespaceSpecifier, + isVariableDeclarator, + isObjectPattern, + isProperty, } from '../node-utils'; export const RULE_NAME = 'prefer-wait-for'; -export type MessageIds = 'preferWaitForMethod' | 'preferWaitForImport'; +export type MessageIds = + | 'preferWaitForMethod' + | 'preferWaitForImport' + | 'preferWaitForRequire'; type Options = []; const DEPRECATED_METHODS = ['wait', 'waitForElement', 'waitForDomChange']; -export default ESLintUtils.RuleCreator(getDocsUrl)({ +export default createTestingLibraryRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -26,6 +35,8 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ preferWaitForMethod: '`{{ methodName }}` is deprecated in favour of `waitFor`', preferWaitForImport: 'import `waitFor` instead of deprecated async utils', + preferWaitForRequire: + 'require `waitFor` instead of deprecated async utils', }, fixable: 'code', @@ -33,7 +44,34 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }, defaultOptions: [], - create(context) { + create(context, _, helpers) { + let addWaitFor = false; + + const reportRequire = (node: TSESTree.ObjectPattern) => { + context.report({ + node: node, + messageId: 'preferWaitForRequire', + fix(fixer) { + const excludedImports = [...DEPRECATED_METHODS, 'waitFor']; + + const newAllRequired = node.properties + .filter( + (s) => + isProperty(s) && + isIdentifier(s.key) && + !excludedImports.includes(s.key.name) + ) + .map( + (s) => ((s as TSESTree.Property).key as TSESTree.Identifier).name + ); + + newAllRequired.push('waitFor'); + + return fixer.replaceText(node, `{ ${newAllRequired.join(',')} }`); + }, + }); + }; + const reportImport = (node: TSESTree.ImportDeclaration) => { context.report({ node: node, @@ -112,46 +150,100 @@ export default ESLintUtils.RuleCreator(getDocsUrl)({ }; return { - 'ImportDeclaration[source.value=/testing-library/]'( - node: TSESTree.ImportDeclaration - ) { - const deprecatedImportSpecifiers = node.specifiers.filter( - (specifier) => - isImportSpecifier(specifier) && - specifier.imported && - DEPRECATED_METHODS.includes(specifier.imported.name) - ); - - deprecatedImportSpecifiers.forEach((importSpecifier, i) => { - if (i === 0) { - reportImport(node); - } - - context - .getDeclaredVariables(importSpecifier) - .forEach((variable) => - variable.references.forEach((reference) => - reportWait(reference.identifier) - ) - ); - }); + 'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) { + const isDeprecatedMethod = + isIdentifier(node.property) && + DEPRECATED_METHODS.includes(node.property.name); + if (!isDeprecatedMethod) { + // the method does not match a deprecated method + return; + } + const testingLibraryNode = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + // this verifies the owner of the MemberExpression is the same as the node if it was imported with "import * as TL from 'foo'" + const callerIsTestingLibraryFromImport = + isIdentifier(node.object) && + isImportDeclaration(testingLibraryNode) && + isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) && + node.object.name === testingLibraryNode.specifiers[0].local.name; + // this verifies the owner of the MemberExpression is the same as the node if it was imported with "const tl = require('foo')" + const callerIsTestingLibraryFromRequire = + isIdentifier(node.object) && + isCallExpression(testingLibraryNode) && + isVariableDeclarator(testingLibraryNode.parent) && + isIdentifier(testingLibraryNode.parent.id) && + node.object.name === testingLibraryNode.parent.id.name; + if ( + !callerIsTestingLibraryFromImport && + !callerIsTestingLibraryFromRequire + ) { + // the method does not match from the imported elements from TL (even from custom) + return; + } + addWaitFor = true; + reportWait(node.property as TSESTree.Identifier); // compiler is not picking up correctly, it should have inferred it is an identifier }, - 'ImportDeclaration[source.value=/testing-library/] > ImportNamespaceSpecifier'( - node: TSESTree.ImportNamespaceSpecifier - ) { - context.getDeclaredVariables(node).forEach((variable) => - variable.references.forEach((reference) => { - if ( - isMemberExpression(reference.identifier.parent) && - isIdentifier(reference.identifier.parent.property) && - DEPRECATED_METHODS.includes( - reference.identifier.parent.property.name - ) - ) { - reportWait(reference.identifier.parent.property); - } - }) - ); + 'CallExpression > Identifier'(node: TSESTree.Identifier) { + const testingLibraryNode = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + // this verifies the owner of the MemberExpression is the same as the node if it was imported with "import { deprecated as aliased } from 'foo'" + const callerIsTestingLibraryFromImport = + isImportDeclaration(testingLibraryNode) && + testingLibraryNode.specifiers.some( + (s) => + isImportSpecifier(s) && + DEPRECATED_METHODS.includes(s.imported.name) && + s.local.name === node.name + ); + // this verifies the owner of the MemberExpression is the same as the node if it was imported with "const { deprecatedMethod } = require('foo')" + const callerIsTestingLibraryFromRequire = + isCallExpression(testingLibraryNode) && + isVariableDeclarator(testingLibraryNode.parent) && + isObjectPattern(testingLibraryNode.parent.id) && + testingLibraryNode.parent.id.properties.some( + (p) => + isProperty(p) && + isIdentifier(p.key) && + isIdentifier(p.value) && + p.value.name === node.name && + DEPRECATED_METHODS.includes(p.key.name) + ); + if ( + !callerIsTestingLibraryFromRequire && + !callerIsTestingLibraryFromImport + ) { + return; + } + addWaitFor = true; + reportWait(node); + }, + 'Program:exit'() { + if (!addWaitFor) { + return; + } + // now that all usages of deprecated methods were replaced, remove the extra imports + const testingLibraryNode = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + if (isCallExpression(testingLibraryNode)) { + const parent = testingLibraryNode.parent as TSESTree.VariableDeclarator; + if (!isObjectPattern(parent.id)) { + // if there is no destructuring, there is nothing to replace + return; + } + reportRequire(parent.id); + } else { + if ( + testingLibraryNode.specifiers.length === 1 && + isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) + ) { + // if we import everything, there is nothing to replace + return; + } + reportImport(testingLibraryNode); + } }, }; }, diff --git a/tests/lib/rules/prefer-wait-for.test.ts b/tests/lib/rules/prefer-wait-for.test.ts index fa0c4c6e..4e7ea7a3 100644 --- a/tests/lib/rules/prefer-wait-for.test.ts +++ b/tests/lib/rules/prefer-wait-for.test.ts @@ -1,33 +1,134 @@ import { createRuleTester } from '../test-utils'; +import { LIBRARY_MODULES } from '../../../lib/utils'; import rule, { RULE_NAME } from '../../../lib/rules/prefer-wait-for'; const ruleTester = createRuleTester(); ruleTester.run(RULE_NAME, rule, { valid: [ + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitFor, render } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitFor, render } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), { - code: `import { waitFor, render } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitFor, render } from 'test-utils'; async () => { await waitFor(() => {}); }`, }, { - code: `import { waitForElementToBeRemoved, render } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitFor, render } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForElementToBeRemoved, render } from '${libraryModule}'; + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForElementToBeRemoved, render } = require('${libraryModule}'); + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForElementToBeRemoved, render } from 'test-utils'; + + async () => { + await waitForElementToBeRemoved(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForElementToBeRemoved, render } = require('test-utils'); async () => { await waitForElementToBeRemoved(() => {}); }`, }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + }, { - code: `import * as testingLibrary from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); async () => { await testingLibrary.waitForElementToBeRemoved(() => {}); }`, }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render } from '${libraryModule}'; + import { waitForSomethingElse } from 'other-module'; + + async () => { + await waitForSomethingElse(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render } = require('${libraryModule}'); + const { waitForSomethingElse } = require('other-module'); + + async () => { + await waitForSomethingElse(() => {}); + }`, + })), { - code: `import { render } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render } from 'test-utils'; import { waitForSomethingElse } from 'other-module'; async () => { @@ -35,8 +136,46 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { - code: `import * as testingLibrary from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render } = require('test-utils'); + const { waitForSomethingElse } = require('other-module'); + + async () => { + await waitForSomethingElse(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); + async () => { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, @@ -48,6 +187,13 @@ ruleTester.run(RULE_NAME, rule, { await wait(); }`, }, + { + code: `const { wait } = require('imNoTestingLibrary'); + + async () => { + await wait(); + }`, + }, { code: `import * as foo from 'imNoTestingLibrary'; @@ -56,13 +202,37 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { - code: ` + code: `const foo = require('imNoTestingLibrary'); + + async () => { + await foo.wait(); + }`, + }, + { + code: `import * as foo from 'imNoTestingLibrary'; + cy.wait(); + `, + }, + { + code: `const foo = require('imNoTestingLibrary'); cy.wait(); `, }, { // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 - code: ` + code: `import * as foo from 'imNoTestingLibrary'; + async function wait(): Promise { + // doesn't matter + } + + function callsWait(): void { + await wait(); + } + `, + }, + { + // https://github.com/testing-library/eslint-plugin-testing-library/issues/145 + code: `const foo = require('imNoTestingLibrary'); async function wait(): Promise { // doesn't matter } @@ -75,9 +245,60 @@ ruleTester.run(RULE_NAME, rule, { ], invalid: [ + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { wait, render } from '${libraryModule}'; + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { wait, render } = require('${libraryModule}'); + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), { - code: `import { wait, render } from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { wait, render } from 'test-utils'; + async () => { await wait(); }`, @@ -93,16 +314,85 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; - + output: `import { render,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { wait, render } = require('test-utils'); + + async () => { + await wait(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('test-utils'); + async () => { await waitFor(() => {}); }`, }, // namespaced wait should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + })), + // namespaced wait should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + })), { - code: `import * as testingLibrary from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + async () => { await testingLibrary.wait(); }`, @@ -113,16 +403,80 @@ ruleTester.run(RULE_NAME, rule, { column: 30, }, ], - output: `import * as testingLibrary from '@testing-library/foo'; - + output: `import * as testingLibrary from 'test-utils'; + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('test-utils'); + async () => { await testingLibrary.waitFor(() => {}); }`, }, // namespaced waitForDomChange should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '${libraryModule}'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), + // namespaced waitForDomChange should be fixed but not its import + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('${libraryModule}'); + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + })), { - code: `import * as testingLibrary from '@testing-library/foo'; - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import * as testingLibrary from 'test-utils'; + async () => { await testingLibrary.waitForDomChange({ timeout: 500 }); }`, @@ -133,16 +487,37 @@ ruleTester.run(RULE_NAME, rule, { column: 30, }, ], - output: `import * as testingLibrary from '@testing-library/foo'; - + output: `import * as testingLibrary from 'test-utils'; + async () => { await testingLibrary.waitFor(() => {}, { timeout: 500 }); }`, }, { - // this import doesn't have trailing semicolon but fixer adds it - code: `import { render, wait } from '@testing-library/foo' - + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const testingLibrary = require('test-utils'); + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `const testingLibrary = require('test-utils'); + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, wait } from '${libraryModule}' + async () => { await wait(() => {}); }`, @@ -158,71 +533,1124 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; - + output: `import { render,waitFor } from '${libraryModule}'; + async () => { await waitFor(() => {}); }`, - }, - { - code: `import { render, wait, screen } from "@testing-library/foo"; - + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render, wait } = require('${libraryModule}'); + async () => { - await wait(function cb() { - doSomething(); - }); + await wait(() => {}); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, wait } from 'test-utils' + + async () => { + await wait(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, wait } = require('test-utils'); + + async () => { + await wait(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + // this import doesn't have trailing semicolon but fixer adds it + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, wait, screen } from "${libraryModule}"; + + 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 '${libraryModule}'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + // this import doesn't have trailing semicolon but fixer adds it + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, wait, screen } from "${libraryModule}"; + + 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 '${libraryModule}'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, wait, screen } from "test-utils"; + + 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 'test-utils'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, wait, screen } = require('test-utils'); + + async () => { + await wait(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('test-utils'); + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, waitForElement, screen } from '${libraryModule}' + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render, waitForElement, screen } = require('${libraryModule}'); + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, waitForElement, screen } from 'test-utils' + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,screen,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, waitForElement, screen } = require('test-utils'); + + async () => { + await waitForElement(() => {}); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,screen,waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForElement } from '${libraryModule}'; + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForElement } = require('${libraryModule}'); + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForElement } from 'test-utils'; + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForElement } = require('test-utils'); + + async () => { + await waitForElement(function cb() { + doSomething(); + }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(function cb() { + doSomething(); + }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange } from '${libraryModule}'; + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange } = require('${libraryModule}'); + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange } from 'test-utils'; + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange } = require('test-utils'); + + async () => { + await waitForDomChange(); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange } from '${libraryModule}'; + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange } = require('${libraryModule}'); + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange } from 'test-utils'; + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange } = require('test-utils'); + + async () => { + await waitForDomChange(mutationObserverOptions); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}, mutationObserverOptions); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange } from '${libraryModule}'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange } = require('${libraryModule}'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange } from 'test-utils'; + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange } = require('test-utils'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { waitFor } = require('test-utils'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange, wait, waitForElement } from '${libraryModule}'; + 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 '${libraryModule}'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange, wait, waitForElement } = require('${libraryModule}'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + 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: `const { waitFor } = require('${libraryModule}'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange, wait, waitForElement } from 'test-utils'; + 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 'test-utils'; + import userEvent from '@testing-library/user-event'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange, wait, waitForElement } = require('test-utils'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + 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: `const { waitFor } = require('test-utils'); + const userEvent = require('@testing-library/user-event'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { render, waitForDomChange, wait, waitForElement } from '${libraryModule}'; + + 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 '${libraryModule}'; + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { render, waitForDomChange, wait, waitForElement } = require('${libraryModule}'); + + async () => { + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + 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: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); + }`, + })), + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { render, waitForDomChange, wait, waitForElement } from 'test-utils'; + + 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: 4, + line: 7, column: 15, }, ], - output: `import { render,screen,waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; async () => { - await waitFor(function cb() { - doSomething(); - }); + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, { - code: `import { render, waitForElement, screen } from '@testing-library/foo' + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { render, waitForDomChange, wait, waitForElement } = require('test-utils'); async () => { - await waitForElement(() => {}); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { 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,screen,waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('test-utils'); async () => { + await waitFor(() => {}, { timeout: 5000 }); await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, - { - code: `import { waitForElement } from '@testing-library/foo'; + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { waitForDomChange, wait, render, waitForElement } from '${libraryModule}'; async () => { - await waitForElement(function cb() { - doSomething(); - }); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { @@ -235,44 +1663,87 @@ ruleTester.run(RULE_NAME, rule, { line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from '${libraryModule}'; async () => { - await waitFor(function cb() { - doSomething(); - }); + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, - }, - { - code: `import { waitForDomChange } from '@testing-library/foo'; + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { waitForDomChange, wait, render, waitForElement } = require('${libraryModule}'); async () => { - await waitForDomChange(); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { 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 { waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('${libraryModule}'); async () => { + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, - }, + })), { - code: `import { waitForDomChange } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { waitForDomChange, wait, render, waitForElement } from 'test-utils'; async () => { - await waitForDomChange(mutationObserverOptions); + await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { @@ -285,40 +1756,86 @@ ruleTester.run(RULE_NAME, rule, { line: 4, column: 15, }, + { + messageId: 'preferWaitForMethod', + line: 5, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 6, + column: 15, + }, + { + messageId: 'preferWaitForMethod', + line: 7, + column: 15, + }, ], - output: `import { waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; async () => { - await waitFor(() => {}, mutationObserverOptions); + await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, { - code: `import { waitForDomChange } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange, wait, render, waitForElement } = require('test-utils'); async () => { await waitForDomChange({ timeout: 5000 }); + await waitForElement(); + await wait(); + await wait(() => { doSomething() }); }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { 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 { waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('test-utils'); async () => { await waitFor(() => {}, { timeout: 5000 }); + await waitFor(() => {}); + await waitFor(() => {}); + await waitFor(() => { doSomething() }); }`, }, - { - code: `import { waitForDomChange, wait, waitForElement } from '@testing-library/foo'; - import userEvent from '@testing-library/user-event'; + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `import { + waitForDomChange, + wait, + render, + waitForElement, + } from '${libraryModule}'; async () => { await waitForDomChange({ timeout: 5000 }); @@ -334,27 +1851,26 @@ ruleTester.run(RULE_NAME, rule, { }, { messageId: 'preferWaitForMethod', - line: 5, + line: 9, column: 15, }, { messageId: 'preferWaitForMethod', - line: 6, + line: 10, column: 15, }, { messageId: 'preferWaitForMethod', - line: 7, + line: 11, column: 15, }, { messageId: 'preferWaitForMethod', - line: 8, + line: 12, column: 15, }, ], - output: `import { waitFor } from '@testing-library/foo'; - import userEvent from '@testing-library/user-event'; + output: `import { render,waitFor } from '${libraryModule}'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -362,9 +1878,14 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - }, - { - code: `import { render, waitForDomChange, wait, waitForElement } from '@testing-library/foo'; + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + code: `const { + waitForDomChange, + wait, + render, + waitForElement, + } = require('${libraryModule}'); async () => { await waitForDomChange({ timeout: 5000 }); @@ -374,32 +1895,32 @@ ruleTester.run(RULE_NAME, rule, { }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', - line: 4, + line: 9, column: 15, }, { messageId: 'preferWaitForMethod', - line: 5, + line: 10, column: 15, }, { messageId: 'preferWaitForMethod', - line: 6, + line: 11, column: 15, }, { messageId: 'preferWaitForMethod', - line: 7, + line: 12, column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('${libraryModule}'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -407,9 +1928,17 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => {}); await waitFor(() => { doSomething() }); }`, - }, + })), { - code: `import { waitForDomChange, wait, render, waitForElement } from '@testing-library/foo'; + settings: { + 'testing-library/module': 'test-utils', + }, + code: `import { + waitForDomChange, + wait, + render, + waitForElement, + } from 'test-utils'; async () => { await waitForDomChange({ timeout: 5000 }); @@ -425,26 +1954,26 @@ ruleTester.run(RULE_NAME, rule, { }, { messageId: 'preferWaitForMethod', - line: 4, + line: 9, column: 15, }, { messageId: 'preferWaitForMethod', - line: 5, + line: 10, column: 15, }, { messageId: 'preferWaitForMethod', - line: 6, + line: 11, column: 15, }, { messageId: 'preferWaitForMethod', - line: 7, + line: 12, column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -454,12 +1983,15 @@ ruleTester.run(RULE_NAME, rule, { }`, }, { - code: `import { + settings: { + 'testing-library/module': 'test-utils', + }, + code: `const { waitForDomChange, wait, render, waitForElement, - } from '@testing-library/foo'; + } = require('test-utils'); async () => { await waitForDomChange({ timeout: 5000 }); @@ -469,9 +2001,9 @@ ruleTester.run(RULE_NAME, rule, { }`, errors: [ { - messageId: 'preferWaitForImport', + messageId: 'preferWaitForRequire', line: 1, - column: 1, + column: 7, }, { messageId: 'preferWaitForMethod', @@ -494,7 +2026,7 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `const { render,waitFor } = require('test-utils'); async () => { await waitFor(() => {}, { timeout: 5000 }); @@ -503,9 +2035,66 @@ ruleTester.run(RULE_NAME, rule, { await waitFor(() => { doSomething() }); }`, }, + ...LIBRARY_MODULES.map((libraryModule) => ({ + // if already importing waitFor then it's not imported twice + code: `import { wait, waitFor, render } from '${libraryModule}'; + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForImport', + line: 1, + column: 1, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `import { render,waitFor } from '${libraryModule}'; + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + })), + ...LIBRARY_MODULES.map((libraryModule) => ({ + // if already importing waitFor then it's not imported twice + code: `const { wait, waitFor, render } = require('${libraryModule}'); + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('${libraryModule}'); + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + })), { + settings: { + 'testing-library/module': 'test-utils', + }, // if already importing waitFor then it's not imported twice - code: `import { wait, waitFor, render } from '@testing-library/foo'; + code: `import { wait, waitFor, render } from 'test-utils'; async () => { await wait(); @@ -523,7 +2112,37 @@ ruleTester.run(RULE_NAME, rule, { column: 15, }, ], - output: `import { render,waitFor } from '@testing-library/foo'; + output: `import { render,waitFor } from 'test-utils'; + + async () => { + await waitFor(() => {}); + await waitFor(someCallback); + }`, + }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + // if already importing waitFor then it's not imported twice + code: `const { wait, waitFor, render } = require('test-utils'); + + async () => { + await wait(); + await waitFor(someCallback); + }`, + errors: [ + { + messageId: 'preferWaitForRequire', + line: 1, + column: 7, + }, + { + messageId: 'preferWaitForMethod', + line: 4, + column: 15, + }, + ], + output: `const { render,waitFor } = require('test-utils'); async () => { await waitFor(() => {}); From 97d89d28f3efc5c8611318eedf28e3c13ef0ad01 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Mon, 16 Nov 2020 20:14:16 -0300 Subject: [PATCH 2/4] refactor: generalized util method --- lib/node-utils.ts | 82 ++++++++++++++++++++++++++++++++++++ lib/rules/prefer-wait-for.ts | 59 ++++---------------------- 2 files changed, 90 insertions(+), 51 deletions(-) diff --git a/lib/node-utils.ts b/lib/node-utils.ts index b187e4e0..0b65cc1c 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -5,6 +5,7 @@ import { TSESTree, } from '@typescript-eslint/experimental-utils'; import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; +import { DetectionHelpers } from './detect-testing-library-utils'; export function isCallExpression( node: TSESTree.Node | null | undefined @@ -292,3 +293,84 @@ export function getAssertNodeInfo( return { matcher, isNegated }; } + +/** + * Returns a boolean indicating if the member expression passed as argument comes from a import clause in the provided node + */ +function isMemberFromCallExpressionComingFromImport( + object: TSESTree.Identifier, + importNode: TSESTree.ImportDeclaration +) { + return ( + isImportDeclaration(importNode) && + isImportNamespaceSpecifier(importNode.specifiers[0]) && + object.name === importNode.specifiers[0].local.name + ); +} + +/** + * Returns a boolean indicating if the member expression passed as argument comes from a require clause in the provided node + */ +function isMemberFromCallExpressionComingFromRequire( + object: TSESTree.Identifier, + requireNode: TSESTree.CallExpression +) { + return ( + isCallExpression(requireNode) && + isVariableDeclarator(requireNode.parent) && + isIdentifier(requireNode.parent.id) && + object.name === requireNode.parent.id.name + ); +} + +/** Returns a boolean if the MemberExpression matches the one imported from RTL in a custom/standard import/require clause */ +export function isMemberFromMethodCallFromTestingLibrary( + node: TSESTree.MemberExpression, + helpers: DetectionHelpers +): boolean { + const importOrRequire = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + if (!isIdentifier(node.object)) { + return false; + } + if (isImportDeclaration(importOrRequire)) { + return isMemberFromCallExpressionComingFromImport( + node.object, + importOrRequire + ); + } + return isMemberFromCallExpressionComingFromRequire( + node.object, + importOrRequire + ); +} + +export function isIdentifierInCallExpressionFromTestingLibrary( + node: TSESTree.Identifier, + helpers: DetectionHelpers +): boolean { + const importOrRequire = + helpers.getCustomModuleImportNode() ?? + helpers.getTestingLibraryImportNode(); + if (!importOrRequire) { + return false; + } + if (isImportDeclaration(importOrRequire)) { + return importOrRequire.specifiers.some( + (s) => isImportSpecifier(s) && s.local.name === node.name + ); + } else { + return ( + isVariableDeclarator(importOrRequire.parent) && + isObjectPattern(importOrRequire.parent.id) && + importOrRequire.parent.id.properties.some( + (p) => + isProperty(p) && + isIdentifier(p.key) && + isIdentifier(p.value) && + p.value.name === node.name + ) + ); + } +} diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index 6c61ca80..1b9e120a 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -6,11 +6,11 @@ import { isIdentifier, findClosestCallExpressionNode, isCallExpression, - isImportDeclaration, isImportNamespaceSpecifier, - isVariableDeclarator, isObjectPattern, isProperty, + isMemberFromMethodCallFromTestingLibrary, + isIdentifierInCallExpressionFromTestingLibrary, } from '../node-utils'; export const RULE_NAME = 'prefer-wait-for'; @@ -158,26 +158,7 @@ export default createTestingLibraryRule({ // the method does not match a deprecated method return; } - const testingLibraryNode = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - // this verifies the owner of the MemberExpression is the same as the node if it was imported with "import * as TL from 'foo'" - const callerIsTestingLibraryFromImport = - isIdentifier(node.object) && - isImportDeclaration(testingLibraryNode) && - isImportNamespaceSpecifier(testingLibraryNode.specifiers[0]) && - node.object.name === testingLibraryNode.specifiers[0].local.name; - // this verifies the owner of the MemberExpression is the same as the node if it was imported with "const tl = require('foo')" - const callerIsTestingLibraryFromRequire = - isIdentifier(node.object) && - isCallExpression(testingLibraryNode) && - isVariableDeclarator(testingLibraryNode.parent) && - isIdentifier(testingLibraryNode.parent.id) && - node.object.name === testingLibraryNode.parent.id.name; - if ( - !callerIsTestingLibraryFromImport && - !callerIsTestingLibraryFromRequire - ) { + if (!isMemberFromMethodCallFromTestingLibrary(node, helpers)) { // the method does not match from the imported elements from TL (even from custom) return; } @@ -185,35 +166,11 @@ export default createTestingLibraryRule({ reportWait(node.property as TSESTree.Identifier); // compiler is not picking up correctly, it should have inferred it is an identifier }, 'CallExpression > Identifier'(node: TSESTree.Identifier) { - const testingLibraryNode = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - // this verifies the owner of the MemberExpression is the same as the node if it was imported with "import { deprecated as aliased } from 'foo'" - const callerIsTestingLibraryFromImport = - isImportDeclaration(testingLibraryNode) && - testingLibraryNode.specifiers.some( - (s) => - isImportSpecifier(s) && - DEPRECATED_METHODS.includes(s.imported.name) && - s.local.name === node.name - ); - // this verifies the owner of the MemberExpression is the same as the node if it was imported with "const { deprecatedMethod } = require('foo')" - const callerIsTestingLibraryFromRequire = - isCallExpression(testingLibraryNode) && - isVariableDeclarator(testingLibraryNode.parent) && - isObjectPattern(testingLibraryNode.parent.id) && - testingLibraryNode.parent.id.properties.some( - (p) => - isProperty(p) && - isIdentifier(p.key) && - isIdentifier(p.value) && - p.value.name === node.name && - DEPRECATED_METHODS.includes(p.key.name) - ); - if ( - !callerIsTestingLibraryFromRequire && - !callerIsTestingLibraryFromImport - ) { + if (!DEPRECATED_METHODS.includes(node.name)) { + return; + } + + if (!isIdentifierInCallExpressionFromTestingLibrary(node, helpers)) { return; } addWaitFor = true; From 8ae9d88a92c4018bb3d77b086022eaf5c44f4f64 Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Tue, 17 Nov 2020 19:12:12 -0300 Subject: [PATCH 3/4] refactor: applied feedback from pr --- lib/detect-testing-library-utils.ts | 55 +++++++++++++++++++ lib/node-utils.ts | 82 ----------------------------- lib/rules/prefer-wait-for.ts | 6 +-- 3 files changed, 57 insertions(+), 86 deletions(-) diff --git a/lib/detect-testing-library-utils.ts b/lib/detect-testing-library-utils.ts index 6ca0f180..17c6ab1b 100644 --- a/lib/detect-testing-library-utils.ts +++ b/lib/detect-testing-library-utils.ts @@ -12,6 +12,8 @@ import { isImportNamespaceSpecifier, isImportSpecifier, isProperty, + isCallExpression, + isObjectPattern, } from './node-utils'; import { ABSENCE_MATCHERS, PRESENCE_MATCHERS } from './utils'; @@ -55,6 +57,9 @@ export type DetectionHelpers = { findImportedUtilSpecifier: ( specifierName: string ) => TSESTree.ImportClause | TSESTree.Identifier | undefined; + isNodeComingFromTestingLibrary: ( + node: TSESTree.MemberExpression | TSESTree.Identifier + ) => boolean; }; const DEFAULT_FILENAME_PATTERN = '^.*\\.(test|spec)\\.[jt]sx?$'; @@ -229,6 +234,55 @@ export function detectTestingLibraryUtils< const canReportErrors: DetectionHelpers['canReportErrors'] = () => { return isTestingLibraryImported() && isValidFilename(); }; + /** + * Takes a MemberExpression or an Identifier and verifies if its name comes from the import in TL + * @param node a MemberExpression (in "foo.property" it would be property) or an Identifier (it should be provided from a CallExpression, for example "foo()") + */ + const isNodeComingFromTestingLibrary: DetectionHelpers['isNodeComingFromTestingLibrary'] = ( + node: TSESTree.MemberExpression | TSESTree.Identifier + ) => { + const importOrRequire = + getCustomModuleImportNode() ?? getTestingLibraryImportNode(); + if (!importOrRequire) { + return false; + } + if (ASTUtils.isIdentifier(node)) { + if (isImportDeclaration(importOrRequire)) { + return importOrRequire.specifiers.some( + (s) => isImportSpecifier(s) && s.local.name === node.name + ); + } else { + return ( + ASTUtils.isVariableDeclarator(importOrRequire.parent) && + isObjectPattern(importOrRequire.parent.id) && + importOrRequire.parent.id.properties.some( + (p) => + isProperty(p) && + ASTUtils.isIdentifier(p.key) && + ASTUtils.isIdentifier(p.value) && + p.value.name === node.name + ) + ); + } + } else { + if (!ASTUtils.isIdentifier(node.object)) { + return false; + } + if (isImportDeclaration(importOrRequire)) { + return ( + isImportDeclaration(importOrRequire) && + isImportNamespaceSpecifier(importOrRequire.specifiers[0]) && + node.object.name === importOrRequire.specifiers[0].local.name + ); + } + return ( + isCallExpression(importOrRequire) && + ASTUtils.isVariableDeclarator(importOrRequire.parent) && + ASTUtils.isIdentifier(importOrRequire.parent.id) && + node.object.name === importOrRequire.parent.id.name + ); + } + }; const helpers = { getTestingLibraryImportNode, @@ -244,6 +298,7 @@ export function detectTestingLibraryUtils< isAbsenceAssert, canReportErrors, findImportedUtilSpecifier, + isNodeComingFromTestingLibrary, }; // Instructions for Testing Library detection. diff --git a/lib/node-utils.ts b/lib/node-utils.ts index 0b65cc1c..b187e4e0 100644 --- a/lib/node-utils.ts +++ b/lib/node-utils.ts @@ -5,7 +5,6 @@ import { TSESTree, } from '@typescript-eslint/experimental-utils'; import { RuleContext } from '@typescript-eslint/experimental-utils/dist/ts-eslint'; -import { DetectionHelpers } from './detect-testing-library-utils'; export function isCallExpression( node: TSESTree.Node | null | undefined @@ -293,84 +292,3 @@ export function getAssertNodeInfo( return { matcher, isNegated }; } - -/** - * Returns a boolean indicating if the member expression passed as argument comes from a import clause in the provided node - */ -function isMemberFromCallExpressionComingFromImport( - object: TSESTree.Identifier, - importNode: TSESTree.ImportDeclaration -) { - return ( - isImportDeclaration(importNode) && - isImportNamespaceSpecifier(importNode.specifiers[0]) && - object.name === importNode.specifiers[0].local.name - ); -} - -/** - * Returns a boolean indicating if the member expression passed as argument comes from a require clause in the provided node - */ -function isMemberFromCallExpressionComingFromRequire( - object: TSESTree.Identifier, - requireNode: TSESTree.CallExpression -) { - return ( - isCallExpression(requireNode) && - isVariableDeclarator(requireNode.parent) && - isIdentifier(requireNode.parent.id) && - object.name === requireNode.parent.id.name - ); -} - -/** Returns a boolean if the MemberExpression matches the one imported from RTL in a custom/standard import/require clause */ -export function isMemberFromMethodCallFromTestingLibrary( - node: TSESTree.MemberExpression, - helpers: DetectionHelpers -): boolean { - const importOrRequire = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - if (!isIdentifier(node.object)) { - return false; - } - if (isImportDeclaration(importOrRequire)) { - return isMemberFromCallExpressionComingFromImport( - node.object, - importOrRequire - ); - } - return isMemberFromCallExpressionComingFromRequire( - node.object, - importOrRequire - ); -} - -export function isIdentifierInCallExpressionFromTestingLibrary( - node: TSESTree.Identifier, - helpers: DetectionHelpers -): boolean { - const importOrRequire = - helpers.getCustomModuleImportNode() ?? - helpers.getTestingLibraryImportNode(); - if (!importOrRequire) { - return false; - } - if (isImportDeclaration(importOrRequire)) { - return importOrRequire.specifiers.some( - (s) => isImportSpecifier(s) && s.local.name === node.name - ); - } else { - return ( - isVariableDeclarator(importOrRequire.parent) && - isObjectPattern(importOrRequire.parent.id) && - importOrRequire.parent.id.properties.some( - (p) => - isProperty(p) && - isIdentifier(p.key) && - isIdentifier(p.value) && - p.value.name === node.name - ) - ); - } -} diff --git a/lib/rules/prefer-wait-for.ts b/lib/rules/prefer-wait-for.ts index 1b9e120a..29595583 100644 --- a/lib/rules/prefer-wait-for.ts +++ b/lib/rules/prefer-wait-for.ts @@ -9,8 +9,6 @@ import { isImportNamespaceSpecifier, isObjectPattern, isProperty, - isMemberFromMethodCallFromTestingLibrary, - isIdentifierInCallExpressionFromTestingLibrary, } from '../node-utils'; export const RULE_NAME = 'prefer-wait-for'; @@ -158,7 +156,7 @@ export default createTestingLibraryRule({ // the method does not match a deprecated method return; } - if (!isMemberFromMethodCallFromTestingLibrary(node, helpers)) { + if (!helpers.isNodeComingFromTestingLibrary(node)) { // the method does not match from the imported elements from TL (even from custom) return; } @@ -170,7 +168,7 @@ export default createTestingLibraryRule({ return; } - if (!isIdentifierInCallExpressionFromTestingLibrary(node, helpers)) { + if (!helpers.isNodeComingFromTestingLibrary(node)) { return; } addWaitFor = true; From c13180d43059c787d8791256fd9f12ab70eb699f Mon Sep 17 00:00:00 2001 From: Gonzalo D'Elia Date: Wed, 18 Nov 2020 18:54:31 -0300 Subject: [PATCH 4/4] test: improve coverage --- tests/create-testing-library-rule.test.ts | 20 ++++++++++++++++++++ tests/fake-rule.ts | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/tests/create-testing-library-rule.test.ts b/tests/create-testing-library-rule.test.ts index 426a1601..2cc4836a 100644 --- a/tests/create-testing-library-rule.test.ts +++ b/tests/create-testing-library-rule.test.ts @@ -185,6 +185,16 @@ ruleTester.run(RULE_NAME, rule, { queryByRole('button') `, }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import * as tl from 'test-utils' + const obj = { tl } + obj.tl.waitFor(() => {}) + `, + }, ], invalid: [ // Test Cases for Imports & Filename @@ -516,5 +526,15 @@ ruleTester.run(RULE_NAME, rule, { `, errors: [{ line: 4, column: 7, messageId: 'queryByError' }], }, + { + settings: { + 'testing-library/module': 'test-utils', + }, + code: ` + import * as tl from 'test-utils' + tl.waitFor(() => {}) + `, + errors: [{ line: 3, column: 9, messageId: 'fakeError' }], + }, ], }); diff --git a/tests/fake-rule.ts b/tests/fake-rule.ts index 6bcc18ba..f1284b84 100644 --- a/tests/fake-rule.ts +++ b/tests/fake-rule.ts @@ -73,6 +73,12 @@ export default createTestingLibraryRule({ return { 'CallExpression Identifier': reportCallExpressionIdentifier, MemberExpression: reportMemberExpression, + 'CallExpression > MemberExpression'(node: TSESTree.MemberExpression) { + if (!helpers.isNodeComingFromTestingLibrary(node)) { + return; + } + context.report({ node, messageId: 'fakeError' }); + }, ImportDeclaration: reportImportDeclaration, 'Program:exit'() { const importNode = helpers.getCustomModuleImportNode();