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.
@@ -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 }