From f6a5ffb00fe77f7a2f8105f49838f17ae7cc8da0 Mon Sep 17 00:00:00 2001 From: Alex Kuznetsov Date: Tue, 11 Feb 2020 22:24:44 -0800 Subject: [PATCH 1/2] feat(rule): add 'no-force-true' rule --- README.md | 4 +- docs/rules/no-force-true.md | 50 +++++++++++++++++++ lib/rules/no-force-true.js | 83 ++++++++++++++++++++++++++++++++ tests/lib/rules/no-force-true.js | 48 ++++++++++++++++++ 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 docs/rules/no-force-true.md create mode 100644 lib/rules/no-force-true.js create mode 100644 tests/lib/rules/no-force-true.js diff --git a/README.md b/README.md index 4378f487..b145c794 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-true": "warn" } } ``` @@ -70,6 +71,7 @@ Rules with a check mark (✅) are enabled by default while using the `plugin:cyp | ✅ | [no-unnecessary-waiting](./docs/rules/no-unnecessary-waiting.md) | Prevent waiting for arbitrary time periods | | | [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) | +| | [no-force-true](./docs/rules/no-force-true.md) | Disallow using of 'force: true' option | ## Chai and `no-unused-expressions` diff --git a/docs/rules/no-force-true.md b/docs/rules/no-force-true.md new file mode 100644 index 00000000..5d35a4ad --- /dev/null +++ b/docs/rules/no-force-true.md @@ -0,0 +1,50 @@ +# disallow using of 'force: true' option (no-force-true) + +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). + + +## 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 really need to use. diff --git a/lib/rules/no-force-true.js b/lib/rules/no-force-true.js new file mode 100644 index 00000000..4463e427 --- /dev/null +++ b/lib/rules/no-force-true.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' }) + } + }, + + } + }, +} diff --git a/tests/lib/rules/no-force-true.js b/tests/lib/rules/no-force-true.js new file mode 100644 index 00000000..b06a12fc --- /dev/null +++ b/tests/lib/rules/no-force-true.js @@ -0,0 +1,48 @@ +'use strict' + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-force-true') + +const RuleTester = require('eslint').RuleTester + +const errors = [{ messageId: 'unexpected' }] +const parserOptions = { ecmaVersion: 6 } + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +let ruleTester = new RuleTester() + +ruleTester.run('no-force-true', 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 }, + ], +}) From fd82fd201d2038ef03138afc8d891696136e69a5 Mon Sep 17 00:00:00 2001 From: Ben Kucera <14625260+Bkucera@users.noreply.github.com> Date: Fri, 14 Feb 2020 13:19:46 -0500 Subject: [PATCH 2/2] small cleanup, rename to "no-force" --- README.md | 4 ++-- docs/rules/{no-force-true.md => no-force.md} | 9 +++++---- lib/rules/{no-force-true.js => no-force.js} | 0 tests/lib/rules/{no-force-true.js => no-force.js} | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) rename docs/rules/{no-force-true.md => no-force.md} (80%) rename lib/rules/{no-force-true.js => no-force.js} (100%) rename tests/lib/rules/{no-force-true.js => no-force.js} (95%) diff --git a/README.md b/README.md index b145c794..21ce7fcf 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You can add rules: "cypress/no-assigning-return-values": "error", "cypress/no-unnecessary-waiting": "error", "cypress/assertion-before-screenshot": "warn", - "cypress/no-force-true": "warn" + "cypress/no-force": "warn" } } ``` @@ -69,9 +69,9 @@ 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) | -| | [no-force-true](./docs/rules/no-force-true.md) | Disallow using of 'force: true' option | ## Chai and `no-unused-expressions` diff --git a/docs/rules/no-force-true.md b/docs/rules/no-force.md similarity index 80% rename from docs/rules/no-force-true.md rename to docs/rules/no-force.md index 5d35a4ad..44c3068d 100644 --- a/docs/rules/no-force-true.md +++ b/docs/rules/no-force.md @@ -1,13 +1,14 @@ -# disallow using of 'force: true' option (no-force-true) +# disallow using of 'force: true' option (no-force) -Using force:true on inputs appears to be confusing rather than helpful. +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), +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), @@ -47,4 +48,4 @@ cy.get('input').focus() ## When Not To Use It -If you really need to use. +If you don't mind using `{ force: true }` with action commands, then turn this rule off. diff --git a/lib/rules/no-force-true.js b/lib/rules/no-force.js similarity index 100% rename from lib/rules/no-force-true.js rename to lib/rules/no-force.js diff --git a/tests/lib/rules/no-force-true.js b/tests/lib/rules/no-force.js similarity index 95% rename from tests/lib/rules/no-force-true.js rename to tests/lib/rules/no-force.js index b06a12fc..ad6aa0cd 100644 --- a/tests/lib/rules/no-force-true.js +++ b/tests/lib/rules/no-force.js @@ -4,7 +4,7 @@ // Requirements //------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-force-true') +const rule = require('../../../lib/rules/no-force') const RuleTester = require('eslint').RuleTester @@ -17,7 +17,7 @@ const parserOptions = { ecmaVersion: 6 } let ruleTester = new RuleTester() -ruleTester.run('no-force-true', rule, { +ruleTester.run('no-force', rule, { valid: [ { code: `cy.get('button').click()`, parserOptions },