diff --git a/lib/rules/no-debug.js b/lib/rules/no-debug.js index 417e052b..cfb4ba7f 100644 --- a/lib/rules/no-debug.js +++ b/lib/rules/no-debug.js @@ -45,6 +45,7 @@ module.exports = { } let hasImportedScreen = false; + let wildcardImportName = null; return { VariableDeclarator(node) { @@ -85,19 +86,22 @@ module.exports = { hasImportedScreen = true; } }, - ImportDeclaration(node) { - const screenModuleName = LIBRARY_MODULES_WITH_SCREEN.find( - module => module === node.source.value - ); + // checks if import has shape: + // import { screen } from '@testing-library/dom'; + 'ImportDeclaration ImportSpecifier'(node) { + const importDeclarationNode = node.parent; - if ( - screenModuleName && - node.specifiers.some( - specifier => specifier.imported.name === 'screen' - ) - ) { - hasImportedScreen = true; - } + if (!hasTestingLibraryImportModule(importDeclarationNode)) return; + + hasImportedScreen = node.imported.name === 'screen'; + }, + // checks if import has shape: + // import * as dtl from '@testing-library/dom'; + 'ImportDeclaration ImportNamespaceSpecifier'(node) { + const importDeclarationNode = node.parent; + if (!hasTestingLibraryImportModule(importDeclarationNode)) return; + + wildcardImportName = node.local && node.local.name; }, [`CallExpression > Identifier[name="debug"]`](node) { if (hasDestructuredDebugStatement) { @@ -108,11 +112,31 @@ module.exports = { } }, [`CallExpression > MemberExpression > Identifier[name="debug"]`](node) { - if ( + /* + check if `debug` used following the pattern: + + import { screen } from '@testing-library/dom'; + ... + screen.debug(); + */ + const isScreenDebugUsed = hasImportedScreen && node.parent && - node.parent.object.name === 'screen' - ) { + node.parent.object.name === 'screen'; + + /* + check if `debug` used following the pattern: + + import * as dtl from '@testing-library/dom'; + ... + dtl.debug(); + */ + const isNamespaceDebugUsed = + wildcardImportName && + node.parent && + node.parent.object.name === wildcardImportName; + + if (isScreenDebugUsed || isNamespaceDebugUsed) { context.report({ node, messageId: 'noDebug', @@ -164,3 +188,9 @@ function isRenderVariableDeclarator(node, renderFunctions) { return false; } + +function hasTestingLibraryImportModule(importDeclarationNode) { + return LIBRARY_MODULES_WITH_SCREEN.some( + module => module === importDeclarationNode.source.value + ); +} diff --git a/lib/rules/prefer-wait-for.js b/lib/rules/prefer-wait-for.js index 0313922c..bf67bf43 100644 --- a/lib/rules/prefer-wait-for.js +++ b/lib/rules/prefer-wait-for.js @@ -61,8 +61,10 @@ module.exports = { methodName: node.name, }, fix(fixer) { - const { parent } = node; - const [arg] = parent.arguments; + const callExpressionNode = findClosestCallExpressionNode(node); + const memberExpressionNode = + node.parent.type === 'MemberExpression' && node.parent; + const [arg] = callExpressionNode.arguments; const fixers = []; if (arg) { @@ -79,7 +81,16 @@ module.exports = { } 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(() => {})')); + let methodReplacement = 'waitFor(() => {})'; + + // if wait method used like `foo.wait()` then we need to keep the + // member expression to get `foo.waitFor(() => {})` + if (memberExpressionNode) { + methodReplacement = `${memberExpressionNode.object.name}.${methodReplacement}`; + } + const newText = methodReplacement; + + fixers.push(fixer.replaceText(callExpressionNode, newText)); } return fixers; @@ -90,8 +101,11 @@ module.exports = { return { 'ImportDeclaration[source.value=/testing-library/]'(node) { const importedNames = node.specifiers - .map(specifier => specifier.imported && specifier.imported.name) - .filter(Boolean); + .filter( + specifier => + specifier.type === 'ImportSpecifier' && specifier.imported + ) + .map(specifier => specifier.imported.name); if ( importedNames.some(importedName => @@ -101,7 +115,7 @@ module.exports = { importNodes.push(node); } }, - 'CallExpression > Identifier[name=/^(wait|waitForElement|waitForDomChange)$/]'( + 'CallExpression Identifier[name=/^(wait|waitForElement|waitForDomChange)$/]'( node ) { waitNodes.push(node); @@ -118,3 +132,11 @@ module.exports = { }; }, }; + +function findClosestCallExpressionNode(node) { + if (node.type === 'CallExpression') { + return node; + } + + return findClosestCallExpressionNode(node.parent); +} diff --git a/tests/lib/rules/no-debug.js b/tests/lib/rules/no-debug.js index 9b7ca4ad..de11f27e 100644 --- a/tests/lib/rules/no-debug.js +++ b/tests/lib/rules/no-debug.js @@ -75,6 +75,17 @@ ruleTester.run('no-debug', rule, { { code: `const { queries } = require('@testing-library/dom')`, }, + { + code: `import * as dtl from '@testing-library/dom'; + const foo = dtl.debug; + `, + }, + { + code: ` + import * as foo from '@somewhere/else'; + foo.debug(); + `, + }, { code: `import { queries } from '@testing-library/dom'`, }, @@ -204,5 +215,18 @@ ruleTester.run('no-debug', rule, { }, ], }, + { + code: ` + import * as dtl from '@testing-library/dom'; + dtl.debug(); + `, + errors: [ + { + messageId: 'noDebug', + line: 3, + column: 13, + }, + ], + }, ], }); diff --git a/tests/lib/rules/prefer-wait-for.js b/tests/lib/rules/prefer-wait-for.js index 85212e01..690c7564 100644 --- a/tests/lib/rules/prefer-wait-for.js +++ b/tests/lib/rules/prefer-wait-for.js @@ -22,6 +22,13 @@ ruleTester.run('prefer-wait-for', rule, { await waitForElementToBeRemoved(() => {}); }`, }, + { + code: `import * as testingLibrary from '@testing-library/foo'; + + async () => { + await testingLibrary.waitForElementToBeRemoved(() => {}); + }`, + }, { code: `import { render } from '@testing-library/foo'; import { waitForSomethingElse } from 'other-module'; @@ -30,6 +37,13 @@ ruleTester.run('prefer-wait-for', rule, { await waitForSomethingElse(() => {}); }`, }, + { + code: `import * as testingLibrary from '@testing-library/foo'; + + async () => { + await testingLibrary.waitFor(() => {}, { timeout: 500 }); + }`, + }, ], invalid: [ @@ -57,6 +71,46 @@ ruleTester.run('prefer-wait-for', rule, { await waitFor(() => {}); }`, }, + // namespaced wait should be fixed but not its import + { + code: `import * as testingLibrary from '@testing-library/foo'; + + async () => { + await testingLibrary.wait(); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '@testing-library/foo'; + + async () => { + await testingLibrary.waitFor(() => {}); + }`, + }, + // namespaced waitForDomChange should be fixed but not its import + { + code: `import * as testingLibrary from '@testing-library/foo'; + + async () => { + await testingLibrary.waitForDomChange({ timeout: 500 }); + }`, + errors: [ + { + messageId: 'preferWaitForMethod', + line: 4, + column: 30, + }, + ], + output: `import * as testingLibrary from '@testing-library/foo'; + + 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'