Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/rules/no-and.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# cypress/no-and

📝 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-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
75 changes: 75 additions & 0 deletions lib/rules/no-and.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
}
Comment on lines +34 to +51
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at other PRs and came across this comment.

The approach I took here works and is suitable for this use case but for code cleanliness reasons I expect that I should just use that approach instead. It's likely worth waiting for that to work to be completed and merged in and updating accordingly here.


// ----------------------------------------------------------------------
// 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-and.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-and'),
RuleTester = require('eslint').RuleTester

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

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

ruleTester.run('no-and', 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,
},
],
})