Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rule): add 'no-force' rule #39

Merged
merged 2 commits into from Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 },
],
})