Repository: cypress-io/eslint-plugin-cypress
Pull request #118 (2022-12-28 18:11:12)

feat: Add 'unsafe-to-chain-command' rule


Patch:
diff --git a/README.md b/README.md
index 4ad2b5e..e0e0910 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ Rules with a check mark (✅) are enabled by default while using the `plugin:cyp
 | ✅  | [no-assigning-return-values](./docs/rules/no-assigning-return-values.md)   | Prevent assigning return values of cy calls                     |
 | ✅  | [no-unnecessary-waiting](./docs/rules/no-unnecessary-waiting.md)           | Prevent waiting for arbitrary time periods                      |
 | ✅  | [no-async-tests](./docs/rules/no-async-tests.md)                           | Prevent using async/await in Cypress test case                  |
+| ✅  | [unsafe-to-chain-command](./docs/rules/unsafe-to-chain-command.md)         | Prevent chaining from unsafe to chain commands                  |
 |     | [no-force](./docs/rules/no-force.md)                                       | Disallow using `force: true` with action commands               |
 |     | [assertion-before-screenshot](./docs/rules/assertion-before-screenshot.md) | Ensure screenshots are preceded by an assertion                 |
 |     | [require-data-selectors](./docs/rules/require-data-selectors.md)           | Only allow data-\* attribute selectors (require-data-selectors) |
diff --git a/docs/rules/unsafe-to-chain-command.md b/docs/rules/unsafe-to-chain-command.md
new file mode 100644
index 0000000..5186175
--- /dev/null
+++ b/docs/rules/unsafe-to-chain-command.md
@@ -0,0 +1,3 @@
+## Unsafe to chain command
+
+See [retry-ability guide](https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle).
diff --git a/lib/rules/unsafe-to-chain-command.js b/lib/rules/unsafe-to-chain-command.js
new file mode 100644
index 0000000..c1a9bf0
--- /dev/null
+++ b/lib/rules/unsafe-to-chain-command.js
@@ -0,0 +1,47 @@
+'use strict'
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Actions should be in the end of chains, not in the middle',
+      category: 'Possible Errors',
+      recommended: true,
+      url: 'https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle',
+    },
+    schema: [],
+    messages: {
+      unexpected: 'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.',
+    },
+  },
+  create (context) {
+    return {
+      CallExpression (node) {
+        if (isRootCypress(node) && isActionUnsafeToChain(node) && node.parent.type === 'MemberExpression') {
+          context.report({ node, messageId: 'unexpected' })
+        }
+      },
+    }
+  },
+}
+
+function isRootCypress (node) {
+  while (node.type === 'CallExpression') {
+    if (node.callee.type !== 'MemberExpression') return false
+
+    if (node.callee.object.type === 'Identifier' &&
+        node.callee.object.name === 'cy') {
+      return true
+    }
+
+    node = node.callee.object
+  }
+
+  return false
+}
+
+function isActionUnsafeToChain (node) {
+  // commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx'
+  const unsafeToChainActions = ['blur', 'clear', 'click', 'check', 'dblclick', 'each', 'focus', 'rightclick', 'screenshot', 'scrollIntoView', 'scrollTo', 'select', 'selectFile', 'spread', 'submit', 'type', 'trigger', 'uncheck', 'wait', 'within']
+
+  return node.callee && node.callee.property && node.callee.property.type === 'Identifier' && unsafeToChainActions.includes(node.callee.property.name)
+}


Test patch:
diff --git a/tests/lib/rules/unsafe-to-chain-command.js b/tests/lib/rules/unsafe-to-chain-command.js
new file mode 100644
index 0000000..64326fa
--- /dev/null
+++ b/tests/lib/rules/unsafe-to-chain-command.js
@@ -0,0 +1,20 @@
+'use strict'
+
+const rule = require('../../../lib/rules/unsafe-to-chain-command')
+const RuleTester = require('eslint').RuleTester
+
+const ruleTester = new RuleTester()
+
+const errors = [{ messageId: 'unexpected' }]
+const parserOptions = { ecmaVersion: 6 }
+
+ruleTester.run('action-ends-chain', rule, {
+  valid: [
+    { code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");', parserOptions },
+  ],
+
+  invalid: [
+    { code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");', parserOptions, errors },
+    { code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");', parserOptions, errors },
+  ],
+})