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.
- package/docs/rules/no-and.md +34 -0
- package/lib/index.js +1 -0
- package/lib/rules/assertion-before-screenshot.js +2 -15
- package/lib/rules/no-and.js +56 -0
- package/lib/rules/no-chained-get.js +2 -18
- package/lib/rules/no-debug.js +3 -11
- package/lib/rules/no-force.js +2 -0
- package/lib/rules/no-pause.js +3 -11
- package/lib/rules/no-unnecessary-waiting.js +7 -3
- package/lib/rules/unsafe-to-chain-command.js +1 -22
- package/lib/utils/is-root-cypress.js +28 -0
- package/package.json +1 -1
|
@@ -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
|
package/lib/rules/no-debug.js
CHANGED
|
@@ -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 (
|
|
45
|
+
if (isRootCypress(node) && isCallingDebug(node)) {
|
|
54
46
|
context.report({ node, messageId: 'unexpected' })
|
|
55
47
|
}
|
|
56
48
|
},
|
package/lib/rules/no-force.js
CHANGED
|
@@ -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'
|
package/lib/rules/no-pause.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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 }
|