eslint-plugin-cypress 6.2.2 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ # cypress/no-and
2
+
3
+ 📝 Enforce `.should()` over `.and()` for starting assertion chains.
4
+
5
+ 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ 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.
10
+
11
+ ## Rule Details
12
+
13
+ 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()`.
14
+
15
+ Examples of **incorrect** code for this rule:
16
+
17
+ ```js
18
+ cy.get('elem').and('have.text', 'blah')
19
+ cy.get('foo').find('.bar').and('have.class', 'active')
20
+ cy.get('foo').click().and('be.disabled')
21
+ ```
22
+
23
+ Examples of **correct** code for this rule:
24
+
25
+ ```js
26
+ cy.get('elem').should('have.text', 'blah')
27
+ cy.get('.err').should('be.empty').and('be.hidden')
28
+ cy.contains('Login').and('be.visible')
29
+ cy.get('foo').should('be.visible').and('have.text', 'bar').and('have.class', 'active')
30
+ ```
31
+
32
+ ## When Not To Use It
33
+
34
+ If you prefer using `.and()` interchangeably with `.should()` in all positions, turn this rule off.
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ const plugin = {
6
6
  configs: {},
7
7
  rules: {
8
8
  'assertion-before-screenshot': require('./rules/assertion-before-screenshot'),
9
+ 'no-and': require('./rules/no-and'),
9
10
  'no-assigning-return-values': require('./rules/no-assigning-return-values'),
10
11
  'no-async-before': require('./rules/no-async-before'),
11
12
  'no-async-tests': require('./rules/no-async-tests'),
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
+
3
5
  const assertionCommands = [
4
6
  // assertions
5
7
  'should',
@@ -39,21 +41,6 @@ module.exports = {
39
41
  },
40
42
  }
41
43
 
42
- function isRootCypress(node) {
43
- while (node.type === 'CallExpression') {
44
- if (node.callee.type !== 'MemberExpression') return false
45
-
46
- if (node.callee.object.type === 'Identifier'
47
- && node.callee.object.name === 'cy') {
48
- return true
49
- }
50
-
51
- node = node.callee.object
52
- }
53
-
54
- return false
55
- }
56
-
57
44
  function getPreviousInChain(node) {
58
45
  return node.type === 'CallExpression'
59
46
  && node.callee.type === 'MemberExpression'
@@ -0,0 +1,56 @@
1
+ 'use strict'
2
+
3
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
+
5
+ /** @type {import('eslint').Rule.RuleModule} */
6
+ module.exports = {
7
+ meta: {
8
+ type: 'suggestion',
9
+ docs: {
10
+ description: 'enforce .should() over .and() for starting assertion chains',
11
+ recommended: false,
12
+ url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-and.md',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ unexpected:
18
+ 'Use .should() here; .and() is only allowed after .should(), .and(), or .contains().',
19
+ },
20
+ },
21
+
22
+ create(context) {
23
+ const allowAndAfter = new Set(['should', 'and', 'contains'])
24
+
25
+ function isAllowedAfter(node) {
26
+ const object = node.callee.object
27
+ if (
28
+ object
29
+ && object.type === 'CallExpression'
30
+ && object.callee.type === 'MemberExpression'
31
+ ) {
32
+ return allowAndAfter.has(object.callee.property.name)
33
+ }
34
+ return false
35
+ }
36
+
37
+ return {
38
+ CallExpression(node) {
39
+ if (
40
+ node.callee.type === 'MemberExpression'
41
+ && node.callee.property.name === 'and'
42
+ && isRootCypress(node)
43
+ && !isAllowedAfter(node)
44
+ ) {
45
+ context.report({
46
+ node,
47
+ messageId: 'unexpected',
48
+ fix(fixer) {
49
+ return fixer.replaceText(node.callee.property, 'should')
50
+ },
51
+ })
52
+ }
53
+ },
54
+ }
55
+ },
56
+ }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
+
3
5
  // ------------------------------------------------------------------------------
4
6
  // Rule Definition
5
7
  // ------------------------------------------------------------------------------
@@ -21,24 +23,6 @@ module.exports = {
21
23
  },
22
24
 
23
25
  create(context) {
24
- const isRootCypress = (node) => {
25
- if (
26
- node.type !== 'CallExpression'
27
- || node.callee.type !== 'MemberExpression'
28
- ) {
29
- return false
30
- }
31
-
32
- if (
33
- node.callee.object.type === 'Identifier'
34
- && node.callee.object.name === 'cy'
35
- ) {
36
- return true
37
- }
38
-
39
- return isRootCypress(node.callee.object)
40
- }
41
-
42
26
  const hasChainedGet = (node) => {
43
27
  // Check if this node is a get() call
44
28
  const isGetCall
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
+
3
5
  // ------------------------------------------------------------------------------
4
6
  // Rule Definition
5
7
  // ------------------------------------------------------------------------------
@@ -33,16 +35,6 @@ module.exports = {
33
35
  && node.callee.property.name === 'debug'
34
36
  }
35
37
 
36
- function isCypressCall(node) {
37
- if (!node.callee || node.callee.type !== 'MemberExpression') {
38
- return false
39
- }
40
- if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') {
41
- return true
42
- }
43
- return isCypressCall(node.callee.object)
44
- }
45
-
46
38
  // ----------------------------------------------------------------------
47
39
  // Public
48
40
  // ----------------------------------------------------------------------
@@ -50,7 +42,7 @@ module.exports = {
50
42
  return {
51
43
 
52
44
  CallExpression(node) {
53
- if (isCypressCall(node) && isCallingDebug(node)) {
45
+ if (isRootCypress(node) && isCallingDebug(node)) {
54
46
  context.report({ node, messageId: 'unexpected' })
55
47
  }
56
48
  },
@@ -33,6 +33,8 @@ module.exports = {
33
33
  && allowedMethods.includes(node.property.name)
34
34
  }
35
35
 
36
+ // Only the segment whose callee.object is `cy` (not full chain root):
37
+ // deepCheck walks parents from that node to find `.click()` / `{ force }`.
36
38
  function isCypressCall(node) {
37
39
  return node.callee.type === 'MemberExpression'
38
40
  && node.callee.object.type === 'Identifier'
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
+
3
5
  // ------------------------------------------------------------------------------
4
6
  // Rule Definition
5
7
  // ------------------------------------------------------------------------------
@@ -33,16 +35,6 @@ module.exports = {
33
35
  && node.callee.property.name === 'pause'
34
36
  }
35
37
 
36
- function isCypressCall(node) {
37
- if (!node.callee || node.callee.type !== 'MemberExpression') {
38
- return false
39
- }
40
- if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') {
41
- return true
42
- }
43
- return isCypressCall(node.callee.object)
44
- }
45
-
46
38
  // ----------------------------------------------------------------------
47
39
  // Public
48
40
  // ----------------------------------------------------------------------
@@ -50,7 +42,7 @@ module.exports = {
50
42
  return {
51
43
 
52
44
  CallExpression(node) {
53
- if (isCypressCall(node) && isCallingPause(node)) {
45
+ if (isRootCypress(node) && isCallingPause(node)) {
54
46
  context.report({ node, messageId: 'unexpected' })
55
47
  }
56
48
  },
@@ -79,13 +79,17 @@ function isIdentifierNumberConstArgument(node, scope) {
79
79
  // we don't know if WAIT is a number or alias '@someRequest', so don't fail
80
80
  if (definition.type === 'ImportBinding') return false
81
81
 
82
- const param = definition.node.params[definition.index]
82
+ // Get the identifier's parent node to check for a default value.
83
+ // For `function wait (amount = 1) { ... }`, `definition.name` is the `amount` Identifier,
84
+ // and its parent is the AssignmentPattern `amount = 1`.
85
+ // This also handles destructured parameters like `function wait ({ ms = 1 }) { ... }`.
86
+ const nameParent = definition.name.parent
83
87
 
84
88
  // function wait (amount) { cy.wait(amount) }
85
89
  // we can't know the type of value, so don't fail
86
- if (!param || param.type !== 'AssignmentPattern') return false
90
+ if (!nameParent || nameParent.type !== 'AssignmentPattern') return false
87
91
 
88
92
  // function wait (amount = 1) { cy.wait(amount) } or
89
93
  // function wait (amount = '@alias') { cy.wait(amount) }
90
- return typeof param.right.value === 'number'
94
+ return typeof nameParent.right.value === 'number'
91
95
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { basename } = require('path')
4
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
5
 
5
6
  const NAME = basename(__dirname)
6
7
  const DESCRIPTION = 'disallow actions within chains'
@@ -103,28 +104,6 @@ module.exports = {
103
104
  },
104
105
  }
105
106
 
106
- /**
107
- * @param {import('estree').Node} node
108
- * @returns {boolean}
109
- */
110
- const isRootCypress = (node) => {
111
- if (
112
- node.type !== 'CallExpression'
113
- || node.callee.type !== 'MemberExpression'
114
- ) {
115
- return false
116
- }
117
-
118
- if (
119
- node.callee.object.type === 'Identifier'
120
- && node.callee.object.name === 'cy'
121
- ) {
122
- return true
123
- }
124
-
125
- return isRootCypress(node.callee.object)
126
- }
127
-
128
107
  /**
129
108
  * @param {import('estree').Node} node
130
109
  * @param {(string | RegExp)[]} additionalMethods
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Whether `node` is a CallExpression that belongs to a Cypress command chain
5
+ * rooted at `cy` (walks `callee.object` through nested calls).
6
+ *
7
+ * @param {import('estree').Node} node
8
+ * @returns {boolean}
9
+ */
10
+ function isRootCypress(node) {
11
+ if (
12
+ node.type !== 'CallExpression'
13
+ || node.callee.type !== 'MemberExpression'
14
+ ) {
15
+ return false
16
+ }
17
+
18
+ if (
19
+ node.callee.object.type === 'Identifier'
20
+ && node.callee.object.name === 'cy'
21
+ ) {
22
+ return true
23
+ }
24
+
25
+ return isRootCypress(node.callee.object)
26
+ }
27
+
28
+ module.exports = { isRootCypress }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-cypress",
3
- "version": "6.2.2",
3
+ "version": "6.3.0",
4
4
  "description": "An ESLint plugin for projects using Cypress",
5
5
  "main": "./lib/index.js",
6
6
  "exports": {