Skip to content

Commit f43c64e

Browse files
kuzzakakuceb
andauthored
feat(rule): add 'no-force-true' rule (#39)
Co-authored-by: Ben Kucera <[email protected]>
1 parent c3f1b3d commit f43c64e

File tree

4 files changed

+185
-1
lines changed

4 files changed

+185
-1
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ You can add rules:
2929
"rules": {
3030
"cypress/no-assigning-return-values": "error",
3131
"cypress/no-unnecessary-waiting": "error",
32-
"cypress/assertion-before-screenshot": "warn"
32+
"cypress/assertion-before-screenshot": "warn",
33+
"cypress/no-force": "warn"
3334
}
3435
}
3536
```
@@ -68,6 +69,7 @@ Rules with a check mark (✅) are enabled by default while using the `plugin:cyp
6869
| :-- | :------------------------------------------------------------------------- | :-------------------------------------------------------------- |
6970
|| [no-assigning-return-values](./docs/rules/no-assigning-return-values.md) | Prevent assigning return values of cy calls |
7071
|| [no-unnecessary-waiting](./docs/rules/no-unnecessary-waiting.md) | Prevent waiting for arbitrary time periods |
72+
| | [no-force](./docs/rules/no-force.md) | Disallow using `force: true` with action commands |
7173
| | [assertion-before-screenshot](./docs/rules/assertion-before-screenshot.md) | Ensure screenshots are preceded by an assertion |
7274
| | [require-data-selectors](./docs/rules/require-data-selectors.md) | Only allow data-\* attribute selectors (require-data-selectors) |
7375

docs/rules/no-force.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# disallow using of 'force: true' option (no-force)
2+
3+
Using `force: true` on inputs appears to be confusing rather than helpful.
4+
It usually silences the actual problem instead of providing a way to overcome it.
5+
See [Cypress Core Concepts](https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Forcing).
6+
7+
If enabling this rule, it's recommended to set the severity to `warn`.
8+
9+
## Rule Details
10+
11+
This rule aims to disallow using of the `force` option on:[`.click()`](https://on.cypress.io/click),
12+
[`.dblclick()`](https://on.cypress.io/dblclick), [`.type()`](https://on.cypress.io/type),
13+
[`.rightclick()`](https://on.cypress.io/rightclick), [`.select()`](https://on.cypress.io/select),
14+
[`.focus()`](https://on.cypress.io/focus), [`.check()`](https://on.cypress.io/check),
15+
and [`.trigger()`](https://on.cypress.io/trigger).
16+
Examples of **incorrect** code for this rule:
17+
18+
```js
19+
20+
cy.get('button').click({force: true})
21+
cy.get('button').dblclick({force: true})
22+
cy.get('input').type('somth', {force: true})
23+
cy.get('div').find('.foo').find('.bar').trigger('change', {force: true})
24+
cy.get('input').trigger('click', {force: true})
25+
cy.get('input').rightclick({force: true})
26+
cy.get('input').check({force: true})
27+
cy.get('input').select({force: true})
28+
cy.get('input').focus({force: true})
29+
30+
```
31+
32+
Examples of **correct** code for this rule:
33+
34+
```js
35+
36+
cy.get('button').click()
37+
cy.get('button').click({multiple: true})
38+
cy.get('button').dblclick()
39+
cy.get('input').type('somth')
40+
cy.get('input').trigger('click', {anyoption: true})
41+
cy.get('input').rightclick({anyoption: true})
42+
cy.get('input').check()
43+
cy.get('input').select()
44+
cy.get('input').focus()
45+
46+
```
47+
48+
49+
## When Not To Use It
50+
51+
If you don't mind using `{ force: true }` with action commands, then turn this rule off.

lib/rules/no-force.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @fileoverview Disallow using of \'force: true\' option for click and type calls
3+
* @author Alex Kuznetsov
4+
*/
5+
6+
'use strict'
7+
8+
//------------------------------------------------------------------------------
9+
// Rule Definition
10+
//------------------------------------------------------------------------------
11+
12+
module.exports = {
13+
meta: {
14+
docs: {
15+
description: 'Disallow using of \'force: true\' option for click and type calls',
16+
category: 'Possible Errors',
17+
recommended: false,
18+
},
19+
fixable: null, // or "code" or "whitespace"
20+
schema: [],
21+
messages: {
22+
unexpected: 'Do not use force on click and type calls',
23+
},
24+
},
25+
26+
create (context) {
27+
28+
// variables should be defined here
29+
30+
//----------------------------------------------------------------------
31+
// Helpers
32+
//----------------------------------------------------------------------
33+
function isCallingClickOrType (node) {
34+
const allowedMethods = ['click', 'dblclick', 'type', 'trigger', 'check', 'rightclick', 'focus', 'select']
35+
36+
return node.property && node.property.type === 'Identifier' &&
37+
allowedMethods.includes(node.property.name)
38+
}
39+
40+
function isCypressCall (node) {
41+
return node.callee.type === 'MemberExpression' &&
42+
node.callee.object.type === 'Identifier' &&
43+
node.callee.object.name === 'cy'
44+
}
45+
46+
function hasOptionForce (node) {
47+
48+
return node.arguments && node.arguments.length &&
49+
node.arguments.some((arg) => {
50+
return arg.type === 'ObjectExpression' && arg.properties.some((propNode) => propNode.key.name === 'force')
51+
})
52+
}
53+
54+
function deepCheck (node, checkFunc) {
55+
let currentNode = node
56+
57+
while (currentNode.parent) {
58+
59+
if (checkFunc(currentNode.parent)) {
60+
return true
61+
}
62+
63+
currentNode = currentNode.parent
64+
}
65+
66+
return false
67+
}
68+
69+
//----------------------------------------------------------------------
70+
// Public
71+
//----------------------------------------------------------------------
72+
73+
return {
74+
75+
CallExpression (node) {
76+
if (isCypressCall(node) && deepCheck(node, isCallingClickOrType) && deepCheck(node, hasOptionForce)) {
77+
context.report({ node, messageId: 'unexpected' })
78+
}
79+
},
80+
81+
}
82+
},
83+
}

tests/lib/rules/no-force.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
//------------------------------------------------------------------------------
4+
// Requirements
5+
//------------------------------------------------------------------------------
6+
7+
const rule = require('../../../lib/rules/no-force')
8+
9+
const RuleTester = require('eslint').RuleTester
10+
11+
const errors = [{ messageId: 'unexpected' }]
12+
const parserOptions = { ecmaVersion: 6 }
13+
14+
//------------------------------------------------------------------------------
15+
// Tests
16+
//------------------------------------------------------------------------------
17+
18+
let ruleTester = new RuleTester()
19+
20+
ruleTester.run('no-force', rule, {
21+
22+
valid: [
23+
{ code: `cy.get('button').click()`, parserOptions },
24+
{ code: `cy.get('button').click({multiple: true})`, parserOptions },
25+
{ code: `cy.get('button').dblclick()`, parserOptions },
26+
{ code: `cy.get('input').type('somth')`, parserOptions },
27+
{ code: `cy.get('input').type('somth', {anyoption: true})`, parserOptions, errors },
28+
{ code: `cy.get('input').trigger('click', {anyoption: true})`, parserOptions, errors },
29+
{ code: `cy.get('input').rightclick({anyoption: true})`, parserOptions, errors },
30+
{ code: `cy.get('input').check()`, parserOptions, errors },
31+
{ code: `cy.get('input').select()`, parserOptions, errors },
32+
{ code: `cy.get('input').focus()`, parserOptions, errors },
33+
],
34+
35+
invalid: [
36+
{ code: `cy.get('button').click({force: true})`, parserOptions, errors },
37+
{ code: `cy.get('button').dblclick({force: true})`, parserOptions, errors },
38+
{ code: `cy.get('input').type('somth', {force: true})`, parserOptions, errors },
39+
{ code: `cy.get('div').find('.foo').type('somth', {force: true})`, parserOptions, errors },
40+
{ code: `cy.get('div').find('.foo').find('.bar').click({force: true})`, parserOptions, errors },
41+
{ code: `cy.get('div').find('.foo').find('.bar').trigger('change', {force: true})`, parserOptions, errors },
42+
{ code: `cy.get('input').trigger('click', {force: true})`, parserOptions, errors },
43+
{ code: `cy.get('input').rightclick({force: true})`, parserOptions, errors },
44+
{ code: `cy.get('input').check({force: true})`, parserOptions, errors },
45+
{ code: `cy.get('input').select({force: true})`, parserOptions, errors },
46+
{ code: `cy.get('input').focus({force: true})`, parserOptions, errors },
47+
],
48+
})

0 commit comments

Comments
 (0)