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

📝 Disallow the use of `.and()`.

🔧 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 `.should()` consistently makes assertions easier to read and avoids ambiguity.

## Rule Details

This rule disallows the use of `.and()` in Cypress chains and auto-fixes it to `.should()`.

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

```js
cy.get('foo').and('be.visible')
cy.get('foo').should('be.visible').and('have.text', 'bar')
cy.contains('Submit').and('be.disabled')
cy.get('input').invoke('val').and('eq', 'hello')
```

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

```js
cy.get('foo').should('be.visible')
cy.get('foo').should('be.visible').should('have.text', 'bar')
cy.contains('Submit').should('be.disabled')
cy.get('input').invoke('val').should('eq', 'hello')
```

## When Not To Use It

If you prefer using `.and()` for readability in chained assertions, 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-add': require('./rules/no-add'),
'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
75 changes: 75 additions & 0 deletions lib/rules/no-add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* @fileoverview disallow the use of .and()
* @author Todd Kemp
*/
'use strict'

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow the use of .and()',
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: 'code',
schema: [], // Add a schema if the rule has options
messages: {
unexpected: 'Do not use .and(); use .should() instead',
},
},

create(context) {
// variables should be defined here

// ----------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------

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
}

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

return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'and'
&& rootIsCy(node)
) {
context.report({
node,
messageId: 'unexpected',
fix(fixer) {
return fixer.replaceText(node.callee.property, 'should')
},
})
}
},
}
},
}
99 changes: 99 additions & 0 deletions tests/lib/rules/no-add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @fileoverview disallow the use of .and()
* @author Todd Kemp
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

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

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester()
const errors = [{ messageId: 'unexpected' }]

ruleTester.run('no-add', rule, {
valid: [
{ 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: 'someOtherLib.get(\'foo\').find(\'.bar\').filter(\'.baz\').first().and(\'be.visible\')' },
],

invalid: [
{
code: 'cy.and(\'be.visible\')',
output: 'cy.should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'foo\').and(\'be.visible\')',
output: 'cy.get(\'foo\').should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'foo\').should(\'be.visible\').and(\'have.text\', \'bar\')',
output: 'cy.get(\'foo\').should(\'be.visible\').should(\'have.text\', \'bar\')',
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\').find(\'.bar\').filter(\'.baz\').first().and(\'be.visible\')',
output: 'cy.get(\'foo\').find(\'.bar\').filter(\'.baz\').first().should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'foo\').find(\'.bar\').filter(\'.baz\').first().parent().siblings().eq(0).and(\'be.visible\')',
output: 'cy.get(\'foo\').find(\'.bar\').filter(\'.baz\').first().parent().siblings().eq(0).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(\'foo\').then(($el) => { cy.wrap($el).and(\'have.class\', \'active\') })',
output: 'cy.get(\'foo\').then(($el) => { cy.wrap($el).should(\'have.class\', \'active\') })',
errors,
},
{
code: 'cy.get(\'foo\').then(($el) => {}).and(\'be.visible\')',
output: 'cy.get(\'foo\').then(($el) => {}).should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'foo\').as(\'myEl\').and(\'be.visible\')',
output: 'cy.get(\'foo\').as(\'myEl\').should(\'be.visible\')',
errors,
},
{
code: 'cy.get(\'foo\').and(\'be.visible\').and(\'have.text\', \'bar\')',
output: 'cy.get(\'foo\').should(\'be.visible\').should(\'have.text\', \'bar\')',
errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }],
},
{
code: 'cy.contains(\'Submit\').and(\'be.disabled\')',
output: 'cy.contains(\'Submit\').should(\'be.disabled\')',
errors,
},
{
code: 'cy.get(\'input\').invoke(\'val\').and(\'eq\', \'hello\')',
output: 'cy.get(\'input\').invoke(\'val\').should(\'eq\', \'hello\')',
errors,
},
],
})
Loading