From f43c64e078e22dda42a398acdf642da2c9d0cf5c Mon Sep 17 00:00:00 2001 From: Alex Kuznetsov <10024934+kuzzaka@users.noreply.github.com> Date: Fri, 14 Feb 2020 11:05:09 -0800 Subject: [PATCH] feat(rule): add 'no-force-true' rule (#39) Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com> --- README.md | 4 +- docs/rules/no-force.md | 51 +++++++++++++++++++++++ lib/rules/no-force.js | 83 +++++++++++++++++++++++++++++++++++++ tests/lib/rules/no-force.js | 48 +++++++++++++++++++++ 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 docs/rules/no-force.md create mode 100644 lib/rules/no-force.js create mode 100644 tests/lib/rules/no-force.js diff --git a/README.md b/README.md index 4378f487..21ce7fcf 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 00000000..44c3068d --- /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 00000000..4463e427 --- /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' }) + } + }, + + } + }, +} diff --git a/tests/lib/rules/no-force.js b/tests/lib/rules/no-force.js new file mode 100644 index 00000000..ad6aa0cd --- /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 }, + ], +})