diff --git a/README.md b/README.md index 67ce34f..a4ccf58 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ These rules enforce some of the [best practices recommended for using Cypress](h | [no-assigning-return-values](docs/rules/no-assigning-return-values.md) | disallow assigning return values of `cy` calls | ✅ | | [no-async-before](docs/rules/no-async-before.md) | disallow using `async`/`await` in Cypress `before` methods | | | [no-async-tests](docs/rules/no-async-tests.md) | disallow using `async`/`await` in Cypress test cases | ✅ | +| [no-chained-get](docs/rules/no-chained-get.md) | disallow chain of `cy.get()` calls | | | [no-debug](docs/rules/no-debug.md) | disallow using `cy.debug()` calls | | | [no-force](docs/rules/no-force.md) | disallow using `force: true` with action commands | | | [no-pause](docs/rules/no-pause.md) | disallow using `cy.pause()` calls | | diff --git a/docs/rules/no-chained-get.md b/docs/rules/no-chained-get.md new file mode 100644 index 0000000..7cc13c6 --- /dev/null +++ b/docs/rules/no-chained-get.md @@ -0,0 +1,19 @@ +# Disallow chain of `cy.get()` calls (`cypress/no-chained-get`) + + +This rule disallows the usage of chained `.get()` calls as `cy.get()` always starts its search from the cy.root element. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```js +cy.get('parent').get('child') +``` + +Examples of **correct** code for this rule: + +```js +cy.get('parent') + .find('child') +``` diff --git a/legacy.js b/legacy.js index d5a7535..03a1ee6 100644 --- a/legacy.js +++ b/legacy.js @@ -13,6 +13,7 @@ module.exports = { 'no-pause': require('./lib/rules/no-pause'), 'no-debug': require('./lib/rules/no-debug'), 'no-xpath': require('./lib/rules/no-xpath'), + 'no-chained-get': require('./lib/rules/no-chained-get'), }, configs: { recommended: require('./lib/config/recommended'), diff --git a/lib/flat.js b/lib/flat.js index 2e0e9bd..e2f82db 100644 --- a/lib/flat.js +++ b/lib/flat.js @@ -16,6 +16,7 @@ const plugin = { 'no-pause': require('./rules/no-pause'), 'no-debug': require('./rules/no-debug'), 'no-xpath': require('./rules/no-xpath'), + 'no-chained-get': require('./rules/no-chained-get'), }, } diff --git a/lib/rules/no-chained-get.js b/lib/rules/no-chained-get.js new file mode 100644 index 0000000..557d06f --- /dev/null +++ b/lib/rules/no-chained-get.js @@ -0,0 +1,87 @@ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'disallow chain of `cy.get()` calls', + recommended: false, + url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-chained-get.md', + }, + fixable: null, + schema: [], + messages: { + unexpected: 'Avoid chaining multiple cy.get() calls', + }, + }, + + create(context) { + const isRootCypress = (node) => { + if ( + node.type !== 'CallExpression' || + node.callee.type !== 'MemberExpression' + ) { + return false; + } + + if ( + node.callee.object.type === 'Identifier' && + node.callee.object.name === 'cy' + ) { + return true; + } + + return isRootCypress(node.callee.object); + }; + + const hasChainedGet = (node) => { + // Check if this node is a get() call + const isGetCall = + node.callee && + node.callee.type === 'MemberExpression' && + node.callee.property && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'get'; + + if (!isGetCall) { + return false; + } + + const obj = node.callee.object; + + if (obj.type === 'CallExpression') { + const objCallee = obj.callee; + + if ( + objCallee && + objCallee.type === 'MemberExpression' && + objCallee.property && + objCallee.property.type === 'Identifier' && + objCallee.property.name === 'get' + ) { + return true; + } + + return hasChainedGet(obj); + } + + return false; + }; + + return { + CallExpression(node) { + if (isRootCypress(node) && hasChainedGet(node)) { + context.report({ + node, + messageId: 'unexpected', + }); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/no-chained-get.js b/tests/lib/rules/no-chained-get.js new file mode 100644 index 0000000..4d01519 --- /dev/null +++ b/tests/lib/rules/no-chained-get.js @@ -0,0 +1,31 @@ +/** + * @fileoverview disallow chain of `cy.get()` calls + * @author benoit + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-chained-get"), + RuleTester = require("eslint").RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +ruleTester.run("no-chained-get", rule, { + valid: [ + { code: "cy.get('div')" }, + { code: "cy.get('.div').find().get()" }, + { code: "cy.get('input').should('be.disabled')" }, + ], + invalid: [ + { + code: "cy.get('div').get('div')", + errors: [{ messageId: "unexpected" }], + }, + ], +});