Skip to content

Commit

Permalink
feat(rule): add 'no-force-true' rule (#39)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com>
  • Loading branch information
kuzzaka and kuceb committed Feb 14, 2020
1 parent c3f1b3d commit f43c64e
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 1 deletion.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -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"
}
}
```
Expand Down Expand Up @@ -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) |

Expand Down
51 changes: 51 additions & 0 deletions 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.
83 changes: 83 additions & 0 deletions 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' })
}
},

}
},
}
48 changes: 48 additions & 0 deletions 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 },
],
})

0 comments on commit f43c64e

Please sign in to comment.