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
73 changes: 73 additions & 0 deletions lib/rules/no-and.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict'

/** @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) {
function rootIsCy(node) {
let current = node.callee.object
while (current) {
if (current.type === 'Identifier' && current.name === 'cy') {
return true
}
if (current.type === 'CallExpression') {
current = current.callee.object
}
else if (current.type === 'MemberExpression') {
current = current.object
}
else {
break
}
}
return false
}
Comment thread
mschile marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

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'
&& rootIsCy(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.
106 changes: 106 additions & 0 deletions tests/lib/rules/no-and.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict'

const rule = require('../../../lib/rules/no-and'),
RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester()
const errors = [
{
message:
'Use .should() here; .and() is only allowed after .should(), .and(), or .contains().',
},
]

ruleTester.run('no-and', rule, {
valid: [
{ code: 'cy.get(\'elem\').should(\'have.text\', \'blah\')' },
{ code: 'cy.get(\'foo\').should(\'be.visible\')' },
{ code: 'cy.get(\'foo\').should(\'be.visible\').should(\'have.text\', \'bar\')' },
{ code: 'cy.get(\'foo\').find(\'.bar\').should(\'have.class\', \'active\')' },
{ code: 'someOtherLib.and(\'something\')' },
{ code: 'someOtherLib.get(\'foo\').and(\'be.visible\')' },
{ code: 'expect(foo).to.equal(true).and(\'have.text\', \'bar\')' },
{ code: 'cy.get(\'.err\').should(\'be.empty\').and(\'be.hidden\')' },
{ code: 'cy.get(\'foo\').should(\'be.visible\').and(\'have.text\', \'bar\')' },
{ code: 'cy.get(\'foo\').should(\'be.visible\').and(\'have.text\', \'bar\').and(\'have.class\', \'active\')' },
{ code: 'cy.contains(\'Login\').and(\'be.visible\')' },
{ code: 'cy.contains(\'Submit\').and(\'be.disabled\')' },
{ code: 'cy.contains(\'Login\').should(\'be.visible\').and(\'have.class\', \'active\')' },
{ code: 'cy.get(\'foo\').should(($el) => { expect($el).to.have.length(1) }).and(\'be.visible\')' },
{ code: 'cy.contains(\'.nav\', \'Home\').and(\'have.class\', \'active\')' },
{ code: 'cy.contains(\'Save\').should(\'not.be.disabled\').and(\'be.visible\')' },
],

invalid: [
{
code: 'cy.and(\'be.visible\')',
output: 'cy.should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'elem\').and(\'have.text\', \'blah\')',
output: 'cy.get(\'elem\').should(\'have.text\', \'blah\')',
errors,
},
{
code: 'cy.get(\'foo\').and(\'be.visible\')',
output: 'cy.get(\'foo\').should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'foo\').find(\'.bar\').and(\'have.class\', \'active\')',
output: 'cy.get(\'foo\').find(\'.bar\').should(\'have.class\', \'active\')',
errors,
},
{
code: 'cy.get(\'foo\').click().and(\'be.disabled\')',
output: 'cy.get(\'foo\').click().should(\'be.disabled\')',
errors,
},
{
code: 'cy.get(\'input\').invoke(\'val\').and(\'eq\', \'hello\')',
output: 'cy.get(\'input\').invoke(\'val\').should(\'eq\', \'hello\')',
errors,
},
{
code: 'cy.get(\'foo\').then(($el) => {}).and(\'be.visible\')',
output: 'cy.get(\'foo\').then(($el) => {}).should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'.container\').within(() => { cy.get(\'.item\').and(\'be.visible\') })',
output: 'cy.get(\'.container\').within(() => { cy.get(\'.item\').should(\'be.visible\') })',
errors,
},
{
code: 'cy.get(\'input\').type(\'bar\').and(\'have.value\', \'bar\')',
output: 'cy.get(\'input\').type(\'bar\').should(\'have.value\', \'bar\')',
errors,
},
{
code: 'cy.get(\'input\').check().and(\'be.checked\')',
output: 'cy.get(\'input\').check().should(\'be.checked\')',
errors,
},
{
code: 'cy.get(\'select\').select(\'option\').and(\'have.value\', \'option\')',
output: 'cy.get(\'select\').select(\'option\').should(\'have.value\', \'option\')',
errors,
},
{
code: 'cy.wrap(obj).and(\'deep.equal\', expected)',
output: 'cy.wrap(obj).should(\'deep.equal\', expected)',
errors,
},
{
code: 'cy.get(\'foo\').its(\'length\').and(\'eq\', 3)',
output: 'cy.get(\'foo\').its(\'length\').should(\'eq\', 3)',
errors,
},
{
code: 'cy.get(\'foo\').and(\'be.visible\').and(\'have.text\', \'bar\')',
output: 'cy.get(\'foo\').should(\'be.visible\').and(\'have.text\', \'bar\')',
errors,
},
],
})