eslint-plugin-cypress
Advanced tools
Comparing version 2.13.4 to 2.14.0
'use strict' | ||
const { basename } = require('path') | ||
const NAME = basename(__dirname) | ||
const DESCRIPTION = 'Actions should be in the end of chains, not in the middle' | ||
/** | ||
* Commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx.' | ||
* See {@link https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle Actions should be at the end of chains, not the middle} | ||
* for more information. | ||
* | ||
* @type {string[]} | ||
*/ | ||
const unsafeToChainActions = [ | ||
'blur', | ||
'clear', | ||
'click', | ||
'check', | ||
'dblclick', | ||
'each', | ||
'focus', | ||
'rightclick', | ||
'screenshot', | ||
'scrollIntoView', | ||
'scrollTo', | ||
'select', | ||
'selectFile', | ||
'spread', | ||
'submit', | ||
'type', | ||
'trigger', | ||
'uncheck', | ||
'within', | ||
] | ||
/** | ||
* @type {import('eslint').Rule.RuleMetaData['schema']} | ||
*/ | ||
const schema = { | ||
title: NAME, | ||
description: DESCRIPTION, | ||
type: 'object', | ||
properties: { | ||
methods: { | ||
type: 'array', | ||
description: | ||
'An additional list of methods to check for unsafe chaining.', | ||
default: [], | ||
}, | ||
}, | ||
} | ||
/** | ||
* @param {import('eslint').Rule.RuleContext} context | ||
* @returns {Record<string, any>} | ||
*/ | ||
const getDefaultOptions = (context) => { | ||
return Object.entries(schema.properties).reduce((acc, [key, value]) => { | ||
if (!(value.default in value)) return acc | ||
return { | ||
...acc, | ||
[key]: value.default, | ||
} | ||
}, context.options[0] || {}) | ||
} | ||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: 'Actions should be in the end of chains, not in the middle', | ||
description: DESCRIPTION, | ||
category: 'Possible Errors', | ||
@@ -11,12 +78,22 @@ recommended: true, | ||
}, | ||
schema: [], | ||
schema: [schema], | ||
messages: { | ||
unexpected: 'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.', | ||
unexpected: | ||
'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.', | ||
}, | ||
}, | ||
create (context) { | ||
const { methods } = getDefaultOptions(context) | ||
return { | ||
CallExpression (node) { | ||
if (isRootCypress(node) && isActionUnsafeToChain(node) && node.parent.type === 'MemberExpression') { | ||
context.report({ node, messageId: 'unexpected' }) | ||
if ( | ||
isRootCypress(node) && | ||
isActionUnsafeToChain(node, methods) && | ||
node.parent.type === 'MemberExpression' | ||
) { | ||
context.report({ | ||
node, | ||
messageId: 'unexpected', | ||
}) | ||
} | ||
@@ -28,22 +105,40 @@ }, | ||
function isRootCypress (node) { | ||
while (node.type === 'CallExpression') { | ||
if (node.callee.type !== 'MemberExpression') return false | ||
/** | ||
* @param {import('estree').Node} node | ||
* @returns {boolean} | ||
*/ | ||
const isRootCypress = (node) => { | ||
if ( | ||
node.type !== 'CallExpression' || | ||
node.callee.type !== 'MemberExpression' | ||
) { | ||
return false | ||
} | ||
if (node.callee.object.type === 'Identifier' && | ||
node.callee.object.name === 'cy') { | ||
return true | ||
} | ||
node = node.callee.object | ||
if ( | ||
node.callee.object.type === 'Identifier' && | ||
node.callee.object.name === 'cy' | ||
) { | ||
return true | ||
} | ||
return false | ||
return isRootCypress(node.callee.object) | ||
} | ||
function isActionUnsafeToChain (node) { | ||
// commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx' | ||
const unsafeToChainActions = ['blur', 'clear', 'click', 'check', 'dblclick', 'each', 'focus', 'rightclick', 'screenshot', 'scrollIntoView', 'scrollTo', 'select', 'selectFile', 'spread', 'submit', 'type', 'trigger', 'uncheck', 'within'] | ||
/** | ||
* @param {import('estree').Node} node | ||
* @param {(string | RegExp)[]} additionalMethods | ||
*/ | ||
const isActionUnsafeToChain = (node, additionalMethods = []) => { | ||
const unsafeActionsRegex = new RegExp([ | ||
...unsafeToChainActions, | ||
...additionalMethods.map((method) => method instanceof RegExp ? method.source : method), | ||
].join('|')) | ||
return node.callee && node.callee.property && node.callee.property.type === 'Identifier' && unsafeToChainActions.includes(node.callee.property.name) | ||
return ( | ||
node.callee && | ||
node.callee.property && | ||
node.callee.property.type === 'Identifier' && | ||
unsafeActionsRegex.test(node.callee.property.name) | ||
) | ||
} |
{ | ||
"name": "eslint-plugin-cypress", | ||
"version": "2.13.4", | ||
"version": "2.14.0", | ||
"description": "An ESLint plugin for projects using Cypress", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -13,9 +13,32 @@ 'use strict' | ||
valid: [ | ||
{ code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");', parserOptions }, | ||
{ | ||
code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");', | ||
parserOptions, | ||
}, | ||
], | ||
invalid: [ | ||
{ code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");', parserOptions, errors }, | ||
{ code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");', parserOptions, errors }, | ||
{ | ||
code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");', | ||
parserOptions, | ||
errors, | ||
}, | ||
{ | ||
code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");', | ||
parserOptions, | ||
errors, | ||
}, | ||
{ | ||
code: 'cy.get("new-todo").customType("todo A{enter}").customClick();', | ||
parserOptions, | ||
errors, | ||
options: [{ methods: ['customType', 'customClick'] }], | ||
}, | ||
{ | ||
code: 'cy.get("new-todo").customPress("Enter").customScroll();', | ||
parserOptions, | ||
errors, | ||
options: [{ methods: [/customPress/, /customScroll/] }], | ||
}, | ||
], | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47548
875