eslint-plugin-cypress 0.0.0-dev.1109
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/CONTRIBUTING.md +80 -0
- package/LICENSE +21 -0
- package/README.md +299 -0
- package/docs/rules/assertion-before-screenshot.md +28 -0
- package/docs/rules/no-and.md +37 -0
- package/docs/rules/no-assigning-return-values.md +11 -0
- package/docs/rules/no-async-before.md +52 -0
- package/docs/rules/no-async-tests.md +55 -0
- package/docs/rules/no-chained-get.md +21 -0
- package/docs/rules/no-debug.md +22 -0
- package/docs/rules/no-force.md +51 -0
- package/docs/rules/no-pause.md +22 -0
- package/docs/rules/no-unnecessary-waiting.md +11 -0
- package/docs/rules/no-xpath.md +34 -0
- package/docs/rules/require-data-selectors.md +59 -0
- package/docs/rules/unsafe-to-chain-command.md +33 -0
- package/lib/config/recommended.js +14 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +65 -0
- package/lib/rules/assertion-before-screenshot.js +99 -0
- package/lib/rules/no-and.js +56 -0
- package/lib/rules/no-assigning-return-values.js +68 -0
- package/lib/rules/no-async-before.js +53 -0
- package/lib/rules/no-async-tests.js +53 -0
- package/lib/rules/no-chained-get.js +71 -0
- package/lib/rules/no-debug.js +52 -0
- package/lib/rules/no-force.js +79 -0
- package/lib/rules/no-pause.js +52 -0
- package/lib/rules/no-unnecessary-waiting.js +95 -0
- package/lib/rules/no-xpath.js +43 -0
- package/lib/rules/require-data-selectors.js +77 -0
- package/lib/rules/unsafe-to-chain-command.js +123 -0
- package/lib/utils/is-root-cypress.js +46 -0
- package/package.json +68 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isRootCypress } = require('../utils/is-root-cypress')
|
|
4
|
+
|
|
5
|
+
// ------------------------------------------------------------------------------
|
|
6
|
+
// Rule Definition
|
|
7
|
+
// ------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'disallow chain of `cy.get()` calls',
|
|
15
|
+
recommended: false,
|
|
16
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-chained-get.md',
|
|
17
|
+
},
|
|
18
|
+
fixable: null,
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
unexpected: 'Avoid chaining multiple cy.get() calls',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
const hasChainedGet = (node) => {
|
|
27
|
+
// Check if this node is a get() call
|
|
28
|
+
const isGetCall
|
|
29
|
+
= node.callee
|
|
30
|
+
&& node.callee.type === 'MemberExpression'
|
|
31
|
+
&& node.callee.property
|
|
32
|
+
&& node.callee.property.type === 'Identifier'
|
|
33
|
+
&& node.callee.property.name === 'get'
|
|
34
|
+
|
|
35
|
+
if (!isGetCall) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const obj = node.callee.object
|
|
40
|
+
|
|
41
|
+
if (obj.type === 'CallExpression') {
|
|
42
|
+
const objCallee = obj.callee
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
objCallee
|
|
46
|
+
&& objCallee.type === 'MemberExpression'
|
|
47
|
+
&& objCallee.property
|
|
48
|
+
&& objCallee.property.type === 'Identifier'
|
|
49
|
+
&& objCallee.property.name === 'get'
|
|
50
|
+
) {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return hasChainedGet(obj)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
if (isRootCypress(node) && hasChainedGet(node)) {
|
|
63
|
+
context.report({
|
|
64
|
+
node,
|
|
65
|
+
messageId: 'unexpected',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isRootCypress } = require('../utils/is-root-cypress')
|
|
4
|
+
|
|
5
|
+
// ------------------------------------------------------------------------------
|
|
6
|
+
// Rule Definition
|
|
7
|
+
// ------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'suggestion',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'disallow using `cy.debug()` calls',
|
|
14
|
+
category: 'Possible Errors',
|
|
15
|
+
recommended: false,
|
|
16
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-debug.md',
|
|
17
|
+
},
|
|
18
|
+
fixable: null, // or "code" or "whitespace"
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
unexpected: 'Do not use cy.debug command',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
// variables should be defined here
|
|
27
|
+
|
|
28
|
+
// ----------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ----------------------------------------------------------------------
|
|
31
|
+
function isCallingDebug(node) {
|
|
32
|
+
return node.callee
|
|
33
|
+
&& node.callee.property
|
|
34
|
+
&& node.callee.property.type === 'Identifier'
|
|
35
|
+
&& node.callee.property.name === 'debug'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ----------------------------------------------------------------------
|
|
39
|
+
// Public
|
|
40
|
+
// ----------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
|
|
44
|
+
CallExpression(node) {
|
|
45
|
+
if (isRootCypress(node) && isCallingDebug(node)) {
|
|
46
|
+
context.report({ node, messageId: 'unexpected' })
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// ------------------------------------------------------------------------------
|
|
4
|
+
// Rule Definition
|
|
5
|
+
// ------------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'disallow using `force: true` with action commands',
|
|
12
|
+
category: 'Possible Errors',
|
|
13
|
+
recommended: false,
|
|
14
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-force.md',
|
|
15
|
+
},
|
|
16
|
+
fixable: null, // or "code" or "whitespace"
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
unexpected: 'Do not use force on click and type calls',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
// variables should be defined here
|
|
25
|
+
|
|
26
|
+
// ----------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ----------------------------------------------------------------------
|
|
29
|
+
function isCallingClickOrType(node) {
|
|
30
|
+
const allowedMethods = ['click', 'dblclick', 'type', 'trigger', 'check', 'rightclick', 'focus', 'select']
|
|
31
|
+
|
|
32
|
+
return node.property && node.property.type === 'Identifier'
|
|
33
|
+
&& allowedMethods.includes(node.property.name)
|
|
34
|
+
}
|
|
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 }`.
|
|
38
|
+
function isCypressCall(node) {
|
|
39
|
+
return node.callee.type === 'MemberExpression'
|
|
40
|
+
&& node.callee.object.type === 'Identifier'
|
|
41
|
+
&& node.callee.object.name === 'cy'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasOptionForce(node) {
|
|
45
|
+
return node.arguments && node.arguments.length
|
|
46
|
+
&& node.arguments.some((arg) => {
|
|
47
|
+
return arg.type === 'ObjectExpression' && arg.properties.some((propNode) => propNode.key && propNode.key.name === 'force')
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function deepCheck(node, checkFunc) {
|
|
52
|
+
let currentNode = node
|
|
53
|
+
|
|
54
|
+
while (currentNode.parent) {
|
|
55
|
+
if (checkFunc(currentNode.parent)) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
currentNode = currentNode.parent
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ----------------------------------------------------------------------
|
|
66
|
+
// Public
|
|
67
|
+
// ----------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
|
|
71
|
+
CallExpression(node) {
|
|
72
|
+
if (isCypressCall(node) && deepCheck(node, isCallingClickOrType) && deepCheck(node, hasOptionForce)) {
|
|
73
|
+
context.report({ node, messageId: 'unexpected' })
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isRootCypress } = require('../utils/is-root-cypress')
|
|
4
|
+
|
|
5
|
+
// ------------------------------------------------------------------------------
|
|
6
|
+
// Rule Definition
|
|
7
|
+
// ------------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'suggestion',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'disallow using `cy.pause()` calls',
|
|
14
|
+
category: 'Possible Errors',
|
|
15
|
+
recommended: false,
|
|
16
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-pause.md',
|
|
17
|
+
},
|
|
18
|
+
fixable: null, // or "code" or "whitespace"
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
unexpected: 'Do not use cy.pause command',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
// variables should be defined here
|
|
27
|
+
|
|
28
|
+
// ----------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ----------------------------------------------------------------------
|
|
31
|
+
function isCallingPause(node) {
|
|
32
|
+
return node.callee
|
|
33
|
+
&& node.callee.property
|
|
34
|
+
&& node.callee.property.type === 'Identifier'
|
|
35
|
+
&& node.callee.property.name === 'pause'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ----------------------------------------------------------------------
|
|
39
|
+
// Public
|
|
40
|
+
// ----------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
|
|
44
|
+
CallExpression(node) {
|
|
45
|
+
if (isRootCypress(node) && isCallingPause(node)) {
|
|
46
|
+
context.report({ node, messageId: 'unexpected' })
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'disallow waiting for arbitrary time periods',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: true,
|
|
10
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-unnecessary-waiting.md',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
unexpected: 'Do not wait for arbitrary time periods',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (isCallingCyWait(node)) {
|
|
23
|
+
const scope = sourceCode.getScope
|
|
24
|
+
? sourceCode.getScope(node)
|
|
25
|
+
: context.getScope()
|
|
26
|
+
|
|
27
|
+
if (isIdentifierNumberConstArgument(node, scope) || isNumberArgument(node)) {
|
|
28
|
+
context.report({ node, messageId: 'unexpected' })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nodeIsCalledByCy(node) {
|
|
37
|
+
if (node.type === 'Identifier' && node.name === 'cy') return true
|
|
38
|
+
|
|
39
|
+
if (typeof node.callee === 'undefined' || typeof node.callee.object === 'undefined') {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return nodeIsCalledByCy(node.callee.object)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isCallingCyWait(node) {
|
|
47
|
+
return node.callee.type === 'MemberExpression'
|
|
48
|
+
&& nodeIsCalledByCy(node)
|
|
49
|
+
&& node.callee.property.type === 'Identifier'
|
|
50
|
+
&& node.callee.property.name === 'wait'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isNumberArgument(node) {
|
|
54
|
+
return node.arguments.length > 0
|
|
55
|
+
&& node.arguments[0].type === 'Literal'
|
|
56
|
+
&& typeof (node.arguments[0].value) === 'number'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isIdentifierNumberConstArgument(node, scope) {
|
|
60
|
+
if (node.arguments.length === 0) return false
|
|
61
|
+
|
|
62
|
+
if (node.arguments[0].type !== 'Identifier') return false
|
|
63
|
+
|
|
64
|
+
const identifier = node.arguments[0]
|
|
65
|
+
const resolvedIdentifier = scope.references.find((ref) => ref.identifier === identifier).resolved
|
|
66
|
+
const definition = resolvedIdentifier.defs[0]
|
|
67
|
+
const isVariable = definition.type === 'Variable'
|
|
68
|
+
|
|
69
|
+
// const amount = 1000 or const amount = '@alias'
|
|
70
|
+
// cy.wait(amount)
|
|
71
|
+
if (isVariable) {
|
|
72
|
+
if (!definition.node.init) return false
|
|
73
|
+
|
|
74
|
+
return typeof definition.node.init.value === 'number'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// import { WAIT } from './constants'
|
|
78
|
+
// cy.wait(WAIT)
|
|
79
|
+
// we don't know if WAIT is a number or alias '@someRequest', so don't fail
|
|
80
|
+
if (definition.type === 'ImportBinding') return false
|
|
81
|
+
|
|
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
|
|
87
|
+
|
|
88
|
+
// function wait (amount) { cy.wait(amount) }
|
|
89
|
+
// we can't know the type of value, so don't fail
|
|
90
|
+
if (!nameParent || nameParent.type !== 'AssignmentPattern') return false
|
|
91
|
+
|
|
92
|
+
// function wait (amount = 1) { cy.wait(amount) } or
|
|
93
|
+
// function wait (amount = '@alias') { cy.wait(amount) }
|
|
94
|
+
return typeof nameParent.right.value === 'number'
|
|
95
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'disallow using `cy.xpath()` calls',
|
|
8
|
+
recommended: false,
|
|
9
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-xpath.md',
|
|
10
|
+
},
|
|
11
|
+
fixable: null, // Or `code` or `whitespace`
|
|
12
|
+
schema: [], // Add a schema if the rule has options
|
|
13
|
+
messages: {
|
|
14
|
+
unexpected:
|
|
15
|
+
'cy.xpath() is deprecated and unsupported. '
|
|
16
|
+
+ 'Consider using cy.get() with appropriate selectors instead.',
|
|
17
|
+
},
|
|
18
|
+
deprecated: {
|
|
19
|
+
message:
|
|
20
|
+
'The reason for rule deprecation is the underlying deprecation and removal of support for the `@cypress/xpath` npm module in 2023. '
|
|
21
|
+
+ 'The rule is currently provided as a convenience to identify existing code that requires migration to supported API calls '
|
|
22
|
+
+ 'and it is planned for removal in a future major release of the plugin.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
return {
|
|
28
|
+
CallExpression(node) {
|
|
29
|
+
if (isCallingCyXpath(node)) {
|
|
30
|
+
context.report({ node, messageId: 'unexpected' })
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCallingCyXpath(node) {
|
|
38
|
+
return node.callee.type === 'MemberExpression'
|
|
39
|
+
&& node.callee.object.type === 'Identifier'
|
|
40
|
+
&& node.callee.object.name === 'cy'
|
|
41
|
+
&& node.callee.property.type === 'Identifier'
|
|
42
|
+
&& node.callee.property.name === 'xpath'
|
|
43
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
module.exports = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'suggestion',
|
|
5
|
+
docs: {
|
|
6
|
+
description: 'require `data-*` attribute selectors',
|
|
7
|
+
category: 'Possible Errors',
|
|
8
|
+
recommended: false,
|
|
9
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/require-data-selectors.md',
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
unexpected: 'use data-* attribute selectors instead of classes or tag names',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
const variablesSet = new Set()
|
|
19
|
+
return {
|
|
20
|
+
VariableDeclarator(node) {
|
|
21
|
+
if (node.init && node.id && node.id.type === 'Identifier') {
|
|
22
|
+
if (isDataNode(node.init, variablesSet)) {
|
|
23
|
+
variablesSet.add(node.id.name)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
CallExpression(node) {
|
|
29
|
+
if (isCallingCyGet(node) && !isDataArgument(node, variablesSet)) {
|
|
30
|
+
context.report({ node, messageId: 'unexpected' })
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCallingCyGet(node) {
|
|
38
|
+
return node.callee.type === 'MemberExpression'
|
|
39
|
+
&& node.callee.object.type === 'Identifier'
|
|
40
|
+
&& node.callee.object.name === 'cy'
|
|
41
|
+
&& node.callee.property.type === 'Identifier'
|
|
42
|
+
&& node.callee.property.name === 'get'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isDataArgument(node, dataVariables) {
|
|
46
|
+
if (node.arguments.length === 0) return false
|
|
47
|
+
|
|
48
|
+
const firstArg = node.arguments[0]
|
|
49
|
+
return isDataNode(firstArg, dataVariables)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isDataNode(firstArg, dataVariables) {
|
|
53
|
+
if (firstArg.type === 'Literal') {
|
|
54
|
+
return isAliasOrDataSelector(String(firstArg.value))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (firstArg.type === 'TemplateLiteral') {
|
|
58
|
+
return isAliasOrDataSelector(String(firstArg.quasis[0].value.cooked))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (firstArg.type === 'Identifier') {
|
|
62
|
+
return dataVariables.has(firstArg.name)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (firstArg.type === 'ConditionalExpression') {
|
|
66
|
+
return isDataNode(firstArg.consequent, dataVariables)
|
|
67
|
+
&& isDataNode(firstArg.alternate, dataVariables)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isAliasOrDataSelector(selector) {
|
|
74
|
+
return ['[data-', '@'].some(function (validValue) {
|
|
75
|
+
return selector.startsWith(validValue)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { basename } = require('path')
|
|
4
|
+
const { isRootCypress } = require('../utils/is-root-cypress')
|
|
5
|
+
|
|
6
|
+
const NAME = basename(__dirname)
|
|
7
|
+
const DESCRIPTION = 'disallow actions within chains'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx.'
|
|
11
|
+
* See {@link https://docs.cypress.io/app/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle Actions should be at the end of chains, not the middle}
|
|
12
|
+
* for more information.
|
|
13
|
+
*
|
|
14
|
+
* @type {string[]}
|
|
15
|
+
*/
|
|
16
|
+
const unsafeToChainActions = [
|
|
17
|
+
'blur',
|
|
18
|
+
'clear',
|
|
19
|
+
'click',
|
|
20
|
+
'check',
|
|
21
|
+
'dblclick',
|
|
22
|
+
'each',
|
|
23
|
+
'focus',
|
|
24
|
+
'rightclick',
|
|
25
|
+
'screenshot',
|
|
26
|
+
'scrollIntoView',
|
|
27
|
+
'scrollTo',
|
|
28
|
+
'select',
|
|
29
|
+
'selectFile',
|
|
30
|
+
'spread',
|
|
31
|
+
'submit',
|
|
32
|
+
'type',
|
|
33
|
+
'trigger',
|
|
34
|
+
'uncheck',
|
|
35
|
+
'within',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @type {import('eslint').Rule.RuleMetaData['schema']}
|
|
40
|
+
*/
|
|
41
|
+
const schema = {
|
|
42
|
+
title: NAME,
|
|
43
|
+
description: DESCRIPTION,
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
methods: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description:
|
|
49
|
+
'An additional list of methods to check for unsafe chaining.',
|
|
50
|
+
default: [],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
58
|
+
* @returns {Record<string, any>}
|
|
59
|
+
*/
|
|
60
|
+
const getDefaultOptions = (context) => {
|
|
61
|
+
return Object.entries(schema.properties).reduce((acc, [key, value]) => {
|
|
62
|
+
if (!(value.default in value)) return acc
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...acc,
|
|
66
|
+
[key]: value.default,
|
|
67
|
+
}
|
|
68
|
+
}, context.options[0] || {})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
72
|
+
module.exports = {
|
|
73
|
+
meta: {
|
|
74
|
+
type: 'problem',
|
|
75
|
+
docs: {
|
|
76
|
+
description: DESCRIPTION,
|
|
77
|
+
category: 'Possible Errors',
|
|
78
|
+
recommended: true,
|
|
79
|
+
url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/unsafe-to-chain-command.md',
|
|
80
|
+
},
|
|
81
|
+
schema: [schema],
|
|
82
|
+
messages: {
|
|
83
|
+
unexpected:
|
|
84
|
+
'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
create(context) {
|
|
88
|
+
const { methods } = getDefaultOptions(context)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
CallExpression(node) {
|
|
92
|
+
if (
|
|
93
|
+
isRootCypress(node, context)
|
|
94
|
+
&& isActionUnsafeToChain(node, methods)
|
|
95
|
+
&& node.parent.type === 'MemberExpression'
|
|
96
|
+
) {
|
|
97
|
+
context.report({
|
|
98
|
+
node,
|
|
99
|
+
messageId: 'unexpected',
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {import('estree').Node} node
|
|
109
|
+
* @param {(string | RegExp)[]} additionalMethods
|
|
110
|
+
*/
|
|
111
|
+
const isActionUnsafeToChain = (node, additionalMethods = []) => {
|
|
112
|
+
const unsafeActionsRegex = new RegExp([
|
|
113
|
+
...unsafeToChainActions.map((action) => `^${action}$`),
|
|
114
|
+
...additionalMethods.map((method) => method instanceof RegExp ? method.source : method),
|
|
115
|
+
].join('|'))
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
node.callee
|
|
119
|
+
&& node.callee.property
|
|
120
|
+
&& node.callee.property.type === 'Identifier'
|
|
121
|
+
&& unsafeActionsRegex.test(node.callee.property.name)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
* If type information are available, additionally checks the return type of helper functions.
|
|
7
|
+
*
|
|
8
|
+
* @param {import('estree').Node} node
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
const isRootCypress = (node, context) => {
|
|
12
|
+
if (node.type !== 'CallExpression') {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const calleeType = node.callee.type
|
|
17
|
+
if (calleeType === 'MemberExpression') {
|
|
18
|
+
if (node.callee.object.type === 'Identifier' && node.callee.object.name === 'cy') {
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return isRootCypress(node.callee.object, context)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (calleeType === 'Identifier') {
|
|
26
|
+
const services = context?.sourceCode?.parserServices
|
|
27
|
+
if (services?.program && services.esTreeNodeToTSNodeMap) {
|
|
28
|
+
const checker = services.program.getTypeChecker()
|
|
29
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node)
|
|
30
|
+
if (tsNode) {
|
|
31
|
+
const signature = checker.getResolvedSignature(tsNode)
|
|
32
|
+
if (signature) {
|
|
33
|
+
const returnType = checker.getReturnTypeOfSignature(signature)
|
|
34
|
+
const symbol = returnType.getSymbol()
|
|
35
|
+
if (symbol) {
|
|
36
|
+
return checker.getFullyQualifiedName(symbol) === 'Cypress.Chainable'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { isRootCypress }
|