'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} */ 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: DESCRIPTION, category: 'Possible Errors', recommended: true, url: 'https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle', }, 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.', }, }, create (context) { const { methods } = getDefaultOptions(context) return { CallExpression (node) { if ( isRootCypress(node) && isActionUnsafeToChain(node, methods) && node.parent.type === 'MemberExpression' ) { context.report({ node, messageId: 'unexpected', }) } }, } }, } /** * @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 } return isRootCypress(node.callee.object) } /** * @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' && unsafeActionsRegex.test(node.callee.property.name) ) }