Skip to content
34 changes: 34 additions & 0 deletions docs/rules/no-and.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# cypress/no-and

📝 Enforce `.should()` over `.and()` for starting assertion chains.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Cypress's [.and()](https://on.cypress.io/and) is an alias for [.should()](https://on.cypress.io/should). Using `.and()` reads naturally when it follows `.should()` or `.contains()` — it continues an assertion chain like "should be empty **and** be hidden." In other positions, `.should()` is clearer.

## Rule Details

This rule allows `.and()` only when it immediately follows `.should()`, `.and()`, or `.contains()`. In all other positions, it flags `.and()` and auto-fixes it to `.should()`.

Examples of **incorrect** code for this rule:

```js
cy.get('elem').and('have.text', 'blah')
cy.get('foo').find('.bar').and('have.class', 'active')
cy.get('foo').click().and('be.disabled')
```

Examples of **correct** code for this rule:

```js
cy.get('elem').should('have.text', 'blah')
cy.get('.err').should('be.empty').and('be.hidden')
cy.contains('Login').and('be.visible')
cy.get('foo').should('be.visible').and('have.text', 'bar').and('have.class', 'active')
```

## When Not To Use It

If you prefer using `.and()` interchangeably with `.should()` in all positions, turn this rule off.
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const plugin = {
configs: {},
rules: {
'assertion-before-screenshot': require('./rules/assertion-before-screenshot'),
'no-and': require('./rules/no-and'),
'no-assigning-return-values': require('./rules/no-assigning-return-values'),
'no-async-before': require('./rules/no-async-before'),
'no-async-tests': require('./rules/no-async-tests'),
Expand Down
17 changes: 2 additions & 15 deletions lib/rules/assertion-before-screenshot.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const { isRootCypress } = require('../utils/is-root-cypress')

const assertionCommands = [
// assertions
'should',
Expand Down Expand Up @@ -39,21 +41,6 @@ module.exports = {
},
}

function isRootCypress(node) {
while (node.type === 'CallExpression') {
if (node.callee.type !== 'MemberExpression') return false

if (node.callee.object.type === 'Identifier'
&& node.callee.object.name === 'cy') {
return true
}

node = node.callee.object
}

return false
}

function getPreviousInChain(node) {
return node.type === 'CallExpression'
&& node.callee.type === 'MemberExpression'
Expand Down
56 changes: 56 additions & 0 deletions lib/rules/no-and.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict'

const { isRootCypress } = require('../utils/is-root-cypress')

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce .should() over .and() for starting assertion chains',
recommended: false,
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-and.md',
},
fixable: 'code',
schema: [],
messages: {
unexpected:
'Use .should() here; .and() is only allowed after .should(), .and(), or .contains().',
},
},

create(context) {
const allowAndAfter = new Set(['should', 'and', 'contains'])

function isAllowedAfter(node) {
const object = node.callee.object
if (
object
&& object.type === 'CallExpression'
&& object.callee.type === 'MemberExpression'
) {
return allowAndAfter.has(object.callee.property.name)
}
return false
}

return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'and'
&& isRootCypress(node)
&& !isAllowedAfter(node)
) {
context.report({
node,
messageId: 'unexpected',
fix(fixer) {
return fixer.replaceText(node.callee.property, 'should')
},
})
}
},
}
},
}
Comment thread
cursor[bot] marked this conversation as resolved.
20 changes: 2 additions & 18 deletions lib/rules/no-chained-get.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const { isRootCypress } = require('../utils/is-root-cypress')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand All @@ -21,24 +23,6 @@ module.exports = {
},

create(context) {
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)
}

const hasChainedGet = (node) => {
// Check if this node is a get() call
const isGetCall
Expand Down
14 changes: 3 additions & 11 deletions lib/rules/no-debug.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const { isRootCypress } = require('../utils/is-root-cypress')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -33,24 +35,14 @@ module.exports = {
&& node.callee.property.name === 'debug'
}

function isCypressCall(node) {
if (!node.callee || node.callee.type !== 'MemberExpression') {
return false
}
if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') {
return true
}
return isCypressCall(node.callee.object)
}

// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

return {

CallExpression(node) {
if (isCypressCall(node) && isCallingDebug(node)) {
if (isRootCypress(node) && isCallingDebug(node)) {
context.report({ node, messageId: 'unexpected' })
}
},
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/no-force.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ module.exports = {
&& allowedMethods.includes(node.property.name)
}

// Only the segment whose callee.object is `cy` (not full chain root):
// deepCheck walks parents from that node to find `.click()` / `{ force }`.
function isCypressCall(node) {
return node.callee.type === 'MemberExpression'
&& node.callee.object.type === 'Identifier'
Expand Down
14 changes: 3 additions & 11 deletions lib/rules/no-pause.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const { isRootCypress } = require('../utils/is-root-cypress')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand Down Expand Up @@ -33,24 +35,14 @@ module.exports = {
&& node.callee.property.name === 'pause'
}

function isCypressCall(node) {
if (!node.callee || node.callee.type !== 'MemberExpression') {
return false
}
if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') {
return true
}
return isCypressCall(node.callee.object)
}

// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------

return {

CallExpression(node) {
if (isCypressCall(node) && isCallingPause(node)) {
if (isRootCypress(node) && isCallingPause(node)) {
context.report({ node, messageId: 'unexpected' })
}
},
Expand Down
23 changes: 1 addition & 22 deletions lib/rules/unsafe-to-chain-command.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { basename } = require('path')
const { isRootCypress } = require('../utils/is-root-cypress')

const NAME = basename(__dirname)
const DESCRIPTION = 'disallow actions within chains'
Expand Down Expand Up @@ -103,28 +104,6 @@ module.exports = {
},
}

/**
* @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
Expand Down
28 changes: 28 additions & 0 deletions lib/utils/is-root-cypress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

/**
* Whether `node` is a CallExpression that belongs to a Cypress command chain
* rooted at `cy` (walks `callee.object` through nested calls).
*
* @param {import('estree').Node} node
* @returns {boolean}
*/
function 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)
}

module.exports = { isRootCypress }
Loading