Repository: cypress-io/eslint-plugin-cypress
Pull request #39 (2020-01-30 23:01:12)

feat(rule): add 'no-force-true' rule
Disallow usage of force option on inputs.

abraham: Looks like the readme also needs to be updated to reference this rule.
kuzzaka: Thanks for the suggestions, folks!
Bkucera: do we want to warn for `trigger`, `check`, `rightclick`, `focus`, `select` as well?
kuzzaka: Hmm, good point. I don’t know though if we want it. Anyway it seems east to add, should I?
Bkucera: @kuzzaka sure, lets add it
kuzzaka: @Bkucera done
Bkucera: alright, I'm going to rename this to `no-force` instead of `no-force-true` and merge

Patch:
diff --git a/README.md b/README.md
index 4378f48..21ce7fc 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,8 @@ You can add rules:
   "rules": {
     "cypress/no-assigning-return-values": "error",
     "cypress/no-unnecessary-waiting": "error",
-    "cypress/assertion-before-screenshot": "warn"
+    "cypress/assertion-before-screenshot": "warn",
+    "cypress/no-force": "warn"
   }
 }
 ```
@@ -68,6 +69,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-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/no-force.md b/docs/rules/no-force.md
new file mode 100644
index 0000000..44c3068
--- /dev/null
+++ b/docs/rules/no-force.md
@@ -0,0 +1,51 @@
+# disallow using of 'force: true' option (no-force)
+
+Using `force: true` on inputs appears to be confusing rather than helpful.
+It usually silences the actual problem instead of providing a way to overcome it.
+See [Cypress Core Concepts](https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Forcing).
+
+If enabling this rule, it's recommended to set the severity to `warn`.
+
+## Rule Details
+
+This rule aims to disallow using of the `force` option on:[`.click()`](https://on.cypress.io/click),
+[`.dblclick()`](https://on.cypress.io/dblclick), [`.type()`](https://on.cypress.io/type),
+[`.rightclick()`](https://on.cypress.io/rightclick), [`.select()`](https://on.cypress.io/select),
+[`.focus()`](https://on.cypress.io/focus), [`.check()`](https://on.cypress.io/check),
+and [`.trigger()`](https://on.cypress.io/trigger).
+Examples of **incorrect** code for this rule:
+
+```js
+
+cy.get('button').click({force: true})
+cy.get('button').dblclick({force: true})
+cy.get('input').type('somth', {force: true})
+cy.get('div').find('.foo').find('.bar').trigger('change', {force: true})
+cy.get('input').trigger('click', {force: true})
+cy.get('input').rightclick({force: true})
+cy.get('input').check({force: true})
+cy.get('input').select({force: true})
+cy.get('input').focus({force: true})
+
+```
+
+Examples of **correct** code for this rule:
+
+```js
+
+cy.get('button').click()
+cy.get('button').click({multiple: true})
+cy.get('button').dblclick()
+cy.get('input').type('somth')
+cy.get('input').trigger('click', {anyoption: true})
+cy.get('input').rightclick({anyoption: true})
+cy.get('input').check()
+cy.get('input').select()
+cy.get('input').focus()
+
+```
+
+
+## When Not To Use It
+
+If you don't mind using `{ force: true }` with action commands, then turn this rule off.
diff --git a/lib/rules/no-force.js b/lib/rules/no-force.js
new file mode 100644
index 0000000..4463e42
--- /dev/null
+++ b/lib/rules/no-force.js
@@ -0,0 +1,83 @@
+/**
+ * @fileoverview Disallow using of \'force: true\' option for click and type calls
+ * @author Alex Kuznetsov
+ */
+
+'use strict'
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Disallow using of \'force: true\' option for click and type calls',
+      category: 'Possible Errors',
+      recommended: false,
+    },
+    fixable: null, // or "code" or "whitespace"
+    schema: [],
+    messages: {
+      unexpected: 'Do not use force on click and type calls',
+    },
+  },
+
+  create (context) {
+
+    // variables should be defined here
+
+    //----------------------------------------------------------------------
+    // Helpers
+    //----------------------------------------------------------------------
+    function isCallingClickOrType (node) {
+      const allowedMethods = ['click', 'dblclick', 'type', 'trigger', 'check', 'rightclick', 'focus', 'select']
+
+      return node.property && node.property.type === 'Identifier' &&
+             allowedMethods.includes(node.property.name)
+    }
+
+    function isCypressCall (node) {
+      return node.callee.type === 'MemberExpression' &&
+        node.callee.object.type === 'Identifier' &&
+        node.callee.object.name === 'cy'
+    }
+
+    function hasOptionForce (node) {
+
+      return node.arguments && node.arguments.length &&
+      node.arguments.some((arg) => {
+        return arg.type === 'ObjectExpression' && arg.properties.some((propNode) => propNode.key.name === 'force')
+      })
+    }
+
+    function deepCheck (node, checkFunc) {
+      let currentNode = node
+
+      while (currentNode.parent) {
+
+        if (checkFunc(currentNode.parent)) {
+          return true
+        }
+
+        currentNode = currentNode.parent
+      }
+
+      return false
+    }
+
+    //----------------------------------------------------------------------
+    // Public
+    //----------------------------------------------------------------------
+
+    return {
+
+      CallExpression (node) {
+        if (isCypressCall(node) && deepCheck(node, isCallingClickOrType) && deepCheck(node, hasOptionForce)) {
+          context.report({ node, messageId: 'unexpected' })
+        }
+      },
+
+    }
+  },
+}


Test patch:
diff --git a/tests/lib/rules/no-force.js b/tests/lib/rules/no-force.js
new file mode 100644
index 0000000..ad6aa0c
--- /dev/null
+++ b/tests/lib/rules/no-force.js
@@ -0,0 +1,48 @@
+'use strict'
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-force')
+
+const RuleTester = require('eslint').RuleTester
+
+const errors = [{ messageId: 'unexpected' }]
+const parserOptions = { ecmaVersion: 6 }
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+let ruleTester = new RuleTester()
+
+ruleTester.run('no-force', rule, {
+
+  valid: [
+    { code: `cy.get('button').click()`, parserOptions },
+    { code: `cy.get('button').click({multiple: true})`, parserOptions },
+    { code: `cy.get('button').dblclick()`, parserOptions },
+    { code: `cy.get('input').type('somth')`, parserOptions },
+    { code: `cy.get('input').type('somth', {anyoption: true})`, parserOptions, errors },
+    { code: `cy.get('input').trigger('click', {anyoption: true})`, parserOptions, errors },
+    { code: `cy.get('input').rightclick({anyoption: true})`, parserOptions, errors },
+    { code: `cy.get('input').check()`, parserOptions, errors },
+    { code: `cy.get('input').select()`, parserOptions, errors },
+    { code: `cy.get('input').focus()`, parserOptions, errors },
+  ],
+
+  invalid: [
+    { code: `cy.get('button').click({force: true})`, parserOptions, errors },
+    { code: `cy.get('button').dblclick({force: true})`, parserOptions, errors },
+    { code: `cy.get('input').type('somth', {force: true})`, parserOptions, errors },
+    { code: `cy.get('div').find('.foo').type('somth', {force: true})`, parserOptions, errors },
+    { code: `cy.get('div').find('.foo').find('.bar').click({force: true})`, parserOptions, errors },
+    { code: `cy.get('div').find('.foo').find('.bar').trigger('change', {force: true})`, parserOptions, errors },
+    { code: `cy.get('input').trigger('click', {force: true})`, parserOptions, errors },
+    { code: `cy.get('input').rightclick({force: true})`, parserOptions, errors },
+    { code: `cy.get('input').check({force: true})`, parserOptions, errors },
+    { code: `cy.get('input').select({force: true})`, parserOptions, errors },
+    { code: `cy.get('input').focus({force: true})`, parserOptions, errors },
+  ],
+})