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,51 @@
1
+ # cypress/no-force
2
+
3
+ 📝 Disallow using `force: true` with action commands.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ Using `force: true` on inputs appears to be confusing rather than helpful.
8
+ It usually silences the actual problem instead of providing a way to overcome it.
9
+ See [Cypress Core Concepts](https://docs.cypress.io/app/core-concepts/interacting-with-elements.html#Forcing).
10
+
11
+ If enabling this rule, it's recommended to set the severity to `warn`.
12
+
13
+ ## Rule Details
14
+
15
+ This rule disallows using the `force` option on:[`.click()`](https://on.cypress.io/click),
16
+ [`.dblclick()`](https://on.cypress.io/dblclick), [`.type()`](https://on.cypress.io/type),
17
+ [`.rightclick()`](https://on.cypress.io/rightclick), [`.select()`](https://on.cypress.io/select),
18
+ [`.focus()`](https://on.cypress.io/focus), [`.check()`](https://on.cypress.io/check),
19
+ and [`.trigger()`](https://on.cypress.io/trigger).
20
+
21
+ Examples of **incorrect** code for this rule:
22
+
23
+ ```js
24
+ cy.get('button').click({ force: true })
25
+ cy.get('button').dblclick({ force: true })
26
+ cy.get('input').type('somth', { force: true })
27
+ cy.get('div').find('.foo').find('.bar').trigger('change', { force: true })
28
+ cy.get('input').trigger('click', { force: true })
29
+ cy.get('input').rightclick({ force: true })
30
+ cy.get('input').check({ force: true })
31
+ cy.get('input').select({ force: true })
32
+ cy.get('input').focus({ force: true })
33
+ ```
34
+
35
+ Examples of **correct** code for this rule:
36
+
37
+ ```js
38
+ cy.get('button').click()
39
+ cy.get('button').click({ multiple: true })
40
+ cy.get('button').dblclick()
41
+ cy.get('input').type('somth')
42
+ cy.get('input').trigger('click', { anyoption: true })
43
+ cy.get('input').rightclick({ anyoption: true })
44
+ cy.get('input').check()
45
+ cy.get('input').select()
46
+ cy.get('input').focus()
47
+ ```
48
+
49
+ ## When Not To Use It
50
+
51
+ If you don't mind using `{ force: true }` with action commands, then turn this rule off.
@@ -0,0 +1,22 @@
1
+ # cypress/no-pause
2
+
3
+ 📝 Disallow using `cy.pause()` calls.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ It is recommended to remove any [cy.pause](https://on.cypress.io/pause) commands before committing specs to avoid other developers getting unexpected results.
8
+
9
+ ## Rule Details
10
+
11
+ Examples of **incorrect** code for this rule:
12
+
13
+ ```js
14
+ cy.pause()
15
+ cy.get('selector').pause()
16
+ ```
17
+
18
+ Examples of **correct** code for this rule:
19
+
20
+ ```js
21
+ cy.get('selector')
22
+ ```
@@ -0,0 +1,11 @@
1
+ # cypress/no-unnecessary-waiting
2
+
3
+ 📝 Disallow waiting for arbitrary time periods.
4
+
5
+ 💼 This rule is enabled in the ✅ `recommended` config.
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ ## Further Reading
10
+
11
+ See [the Cypress Best Practices guide](https://on.cypress.io/best-practices#Unnecessary-Waiting).
@@ -0,0 +1,34 @@
1
+ # cypress/no-xpath
2
+
3
+ 📝 Disallow using `cy.xpath()` calls.
4
+
5
+ ❌ This rule is deprecated. The reason for rule deprecation is the underlying deprecation and removal of support for the `@cypress/xpath` npm module in 2023. The rule is currently provided as a convenience to identify existing code that requires migration to supported API calls and it is planned for removal in a future major release of the plugin.
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ This rule disallows the usage of `cy.xpath()` for selecting elements.
10
+
11
+ ## Rule Details
12
+
13
+ Examples of **incorrect** code for this rule:
14
+
15
+ ```js
16
+ cy.xpath('//div[@class=\"container\"]').click()
17
+ ```
18
+
19
+ Examples of **correct** code for this rule:
20
+
21
+ ```js
22
+ cy.get('[data-cy="container"]').click()
23
+ ```
24
+
25
+ ## Migration
26
+
27
+ Both [@cypress/xpath](https://www.npmjs.com/package/@cypress/xpath) and the earlier [cypress-xpath](https://www.npmjs.com/package/cypress-xpath) npm packages are deprecated and no longer supported.
28
+ These packages previously provided the command `cy.xpath()` and they should be uninstalled from the Cypress project after migrating `cy.xpath()` selection to `cy.get()`. See below for more details.
29
+
30
+ After migrating away from `cy.xpath()`, the rule can be set to `off` or can be permanently removed from the ESLint configuration.
31
+
32
+ ## Further Reading
33
+
34
+ See [the Cypress Best Practices guide](https://docs.cypress.io/app/core-concepts/best-practices.html#Selecting-Elements).
@@ -0,0 +1,59 @@
1
+ # cypress/require-data-selectors
2
+
3
+ 📝 Require `data-*` attribute selectors.
4
+
5
+ <!-- end auto-generated rule header -->
6
+
7
+ Require `cy.get` to use only selectors that target `data-*` attributes.
8
+
9
+ > Note: If you use this rule, consider only using the `warn` error level, since using `data-*` attribute selectors may not always be possible.
10
+
11
+ ## Rule Details
12
+
13
+ This rule checks the first parameter of each `cy.get`. It supports template literals, variable (re-)assignments and conditional expressions. For conditionals, all branches must conform to this rule.
14
+
15
+ Cypress aliases (as defined by `cy.as`) are always allowed.
16
+
17
+ Examples of **incorrect** code for this rule:
18
+
19
+ ```js
20
+ cy.get('.a')
21
+ cy.get('[daedta-cy=submit]').click()
22
+ cy.get('[d-cy=submit]')
23
+ cy.get('.btn-large').click()
24
+ cy.get('.btn-.large').click()
25
+ cy.get(condition ? '[data-cy=submit]' : '.btn-large')
26
+
27
+ const CLASS_SELECTOR = '.my-class'
28
+ cy.get(CLASS_SELECTOR)
29
+
30
+ const TYPE = 'submit'
31
+ const SUBMIT_TEMPLATE = `.${TYPE}`
32
+ cy.get(SUBMIT_TEMPLATE)
33
+
34
+ const MY_SUBMIT_TEMPLATE = SUBMIT_TEMPLATE
35
+ cy.get(MY_SUBMIT_TEMPLATE)
36
+ ```
37
+
38
+ Examples of **correct** code for this rule:
39
+
40
+ ```js
41
+ cy.get('[data-cy=submit]').click()
42
+ cy.get('[data-QA=submit]')
43
+ cy.get(`[data-QA=submit]`)
44
+ cy.get(condition ? '[data-cy=submit]' : '[data-QA=submit]')
45
+
46
+ const ASSESSMENT_SUBMIT = '[data-cy=assessment-submit]'
47
+ cy.get(ASSESSMENT_SUBMIT).click()
48
+
49
+ const TYPE = 'submit'
50
+ const SUBMIT_TEMPLATE = `[data-QA=${TYPE}]`
51
+ cy.get(SUBMIT_TEMPLATE)
52
+
53
+ const MY_SUBMIT_TEMPLATE = SUBMIT_TEMPLATE
54
+ cy.get(MY_SUBMIT_TEMPLATE)
55
+ ```
56
+
57
+ ## Further Reading
58
+
59
+ See [the Cypress Best Practices guide](https://docs.cypress.io/app/core-concepts/best-practices.html#Selecting-Elements).
@@ -0,0 +1,33 @@
1
+ # cypress/unsafe-to-chain-command
2
+
3
+ 📝 Disallow actions within chains.
4
+
5
+ 💼 This rule is enabled in the ✅ `recommended` config.
6
+
7
+ <!-- end auto-generated rule header -->
8
+
9
+ ### Options
10
+
11
+ <!-- begin auto-generated rule options list -->
12
+
13
+ | Name | Description | Type | Default |
14
+ | :-------- | :---------------------------------------------------------- | :---- | :------ |
15
+ | `methods` | An additional list of methods to check for unsafe chaining. | Array | `[]` |
16
+
17
+ <!-- end auto-generated rule options list -->
18
+
19
+ ## Typed Linting
20
+
21
+ If [Typed Linting](../../README.md#typed-linting) is enabled, this rule also catches unsafe chaining even when the Cypress chain was started from a helper function.
22
+
23
+ ```js
24
+ function getTodo() {
25
+ return cy.get('.todo')
26
+ }
27
+
28
+ getTodo().type('todo A{enter}').type('todo B{enter}')
29
+ ```
30
+
31
+ ## Further Reading
32
+
33
+ See [retry-ability guide](https://docs.cypress.io/app/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle).
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ plugins: ['cypress'],
5
+ env: {
6
+ 'cypress/globals': true,
7
+ },
8
+ rules: {
9
+ 'cypress/no-assigning-return-values': 'error',
10
+ 'cypress/no-unnecessary-waiting': 'error',
11
+ 'cypress/no-async-tests': 'error',
12
+ 'cypress/unsafe-to-chain-command': 'error',
13
+ },
14
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { ESLint, Linter } from 'eslint'
2
+
3
+ declare const plugin: {
4
+ meta: { name: string, version: string }
5
+ configs: {
6
+ globals: Linter.Config<Linter.RulesRecord>
7
+ recommended: Linter.Config<Linter.RulesRecord>
8
+ }
9
+ rules: NonNullable<ESLint.Plugin['rules']>
10
+ }
11
+
12
+ export = plugin
package/lib/index.js ADDED
@@ -0,0 +1,65 @@
1
+ const globals = require('globals')
2
+ const { name, version } = require('../package.json')
3
+
4
+ const plugin = {
5
+ meta: { name, version },
6
+ configs: {},
7
+ rules: {
8
+ 'assertion-before-screenshot': require('./rules/assertion-before-screenshot'),
9
+ 'no-and': require('./rules/no-and'),
10
+ 'no-assigning-return-values': require('./rules/no-assigning-return-values'),
11
+ 'no-async-before': require('./rules/no-async-before'),
12
+ 'no-async-tests': require('./rules/no-async-tests'),
13
+ 'no-chained-get': require('./rules/no-chained-get'),
14
+ 'no-debug': require('./rules/no-debug'),
15
+ 'no-force': require('./rules/no-force'),
16
+ 'no-pause': require('./rules/no-pause'),
17
+ 'no-unnecessary-waiting': require('./rules/no-unnecessary-waiting'),
18
+ 'no-xpath': require('./rules/no-xpath'),
19
+ 'require-data-selectors': require('./rules/require-data-selectors'),
20
+ 'unsafe-to-chain-command': require('./rules/unsafe-to-chain-command'),
21
+ },
22
+ }
23
+
24
+ const commonGlobals
25
+ = Object.assign({
26
+ cy: false,
27
+ Cypress: false,
28
+ expect: false,
29
+ assert: false,
30
+ chai: false,
31
+ }, globals.browser, globals.mocha)
32
+
33
+ Object.assign(plugin.configs, {
34
+ globals: {
35
+ name: 'cypress/globals',
36
+ plugins: {
37
+ cypress: plugin,
38
+ },
39
+ languageOptions: {
40
+ globals:
41
+ commonGlobals,
42
+ },
43
+ },
44
+ })
45
+
46
+ Object.assign(plugin.configs, {
47
+ recommended: {
48
+ name: 'cypress/recommended',
49
+ plugins: {
50
+ cypress: plugin,
51
+ },
52
+ rules: {
53
+ 'cypress/no-assigning-return-values': 'error',
54
+ 'cypress/no-unnecessary-waiting': 'error',
55
+ 'cypress/no-async-tests': 'error',
56
+ 'cypress/unsafe-to-chain-command': 'error',
57
+ },
58
+ languageOptions: {
59
+ globals:
60
+ commonGlobals,
61
+ },
62
+ },
63
+ })
64
+
65
+ module.exports = plugin
@@ -0,0 +1,99 @@
1
+ 'use strict'
2
+
3
+ const { isRootCypress } = require('../utils/is-root-cypress')
4
+
5
+ const assertionCommands = [
6
+ // assertions
7
+ 'should',
8
+ 'and',
9
+ 'contains',
10
+
11
+ // retries until it gets something
12
+ 'get',
13
+
14
+ // not an assertion, but unlikely to require waiting for render
15
+ 'scrollIntoView',
16
+ 'scrollTo',
17
+ ]
18
+
19
+ module.exports = {
20
+ meta: {
21
+ type: 'problem',
22
+ docs: {
23
+ description: 'require screenshots to be preceded by an assertion',
24
+ category: 'Possible Errors',
25
+ recommended: false,
26
+ url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/assertion-before-screenshot.md',
27
+ },
28
+ schema: [],
29
+ messages: {
30
+ unexpected: 'Make an assertion on the page state before taking a screenshot',
31
+ },
32
+ },
33
+ create(context) {
34
+ return {
35
+ CallExpression(node) {
36
+ if (isCallingCyScreenshot(node) && !isPreviousAnAssertion(node)) {
37
+ context.report({ node, messageId: 'unexpected' })
38
+ }
39
+ },
40
+ }
41
+ },
42
+ }
43
+
44
+ function getPreviousInChain(node) {
45
+ return node.type === 'CallExpression'
46
+ && node.callee.type === 'MemberExpression'
47
+ && node.callee.object.type === 'CallExpression'
48
+ && node.callee.object.callee.type === 'MemberExpression'
49
+ && node.callee.object.callee.property.type === 'Identifier'
50
+ && node.callee.object.callee.property.name
51
+ }
52
+
53
+ function getCallExpressionCypressCommand(node) {
54
+ return isRootCypress(node)
55
+ && node.callee.property.type === 'Identifier'
56
+ && node.callee.property.name
57
+ }
58
+
59
+ function isCallingCyScreenshot(node) {
60
+ return getCallExpressionCypressCommand(node) === 'screenshot'
61
+ }
62
+
63
+ function getPreviousCypressCommand(node) {
64
+ const previousInChain = getPreviousInChain(node)
65
+
66
+ if (previousInChain) {
67
+ return previousInChain
68
+ }
69
+
70
+ while (node.parent && !node.parent.body) {
71
+ node = node.parent
72
+ }
73
+
74
+ if (!node.parent || !node.parent.body) return null
75
+
76
+ const body = node.parent.body.type === 'BlockStatement' ? node.parent.body.body : node.parent.body
77
+
78
+ const index = body.indexOf(node)
79
+
80
+ // in the case of a function declaration it won't be found
81
+ if (index < 0) return null
82
+
83
+ if (index === 0) return getPreviousCypressCommand(node.parent)
84
+
85
+ const previousStatement = body[index - 1]
86
+
87
+ if (previousStatement.type !== 'ExpressionStatement'
88
+ || previousStatement.expression.type !== 'CallExpression') {
89
+ return null
90
+ }
91
+
92
+ return getCallExpressionCypressCommand(previousStatement.expression)
93
+ }
94
+
95
+ function isPreviousAnAssertion(node) {
96
+ const previousCypressCommand = getPreviousCypressCommand(node)
97
+
98
+ return assertionCommands.indexOf(previousCypressCommand) >= 0
99
+ }
@@ -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
+ }
@@ -0,0 +1,68 @@
1
+ 'use strict'
2
+
3
+ // safely get nested object property
4
+ function get(obj, propertyString = '') {
5
+ const properties = propertyString.split('.')
6
+
7
+ for (let i = 0; i < properties.length; i++) {
8
+ const value = (obj || {})[properties[i]]
9
+
10
+ if (value == null) return value
11
+
12
+ obj = value
13
+ }
14
+
15
+ return obj
16
+ }
17
+
18
+ module.exports = {
19
+ meta: {
20
+ type: 'problem',
21
+ docs: {
22
+ description: 'disallow assigning return values of `cy` calls',
23
+ category: 'Possible Errors',
24
+ recommended: true,
25
+ url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-assigning-return-values.md',
26
+ },
27
+ schema: [],
28
+ messages: {
29
+ unexpected: 'Do not assign the return value of a Cypress command',
30
+ },
31
+ },
32
+ create(context) {
33
+ return {
34
+ VariableDeclaration(node) {
35
+ if (node.declarations.some(isCypressCommandDeclaration)) {
36
+ context.report({ node, messageId: 'unexpected' })
37
+ }
38
+ },
39
+ }
40
+ },
41
+ }
42
+
43
+ const allowedCommands = {
44
+ now: true,
45
+ spy: true,
46
+ state: true,
47
+ stub: true,
48
+ }
49
+
50
+ function isCypressCommandDeclaration(declarator) {
51
+ let object = get(declarator, 'init.callee.object')
52
+
53
+ if (!object) return
54
+
55
+ while (object.callee) {
56
+ object = object.callee.object
57
+
58
+ if (!object) return
59
+ }
60
+
61
+ const commandName = get(declarator, 'init.callee.property.name')
62
+
63
+ const parent = get(object, 'parent.property.name') || get(declarator, 'id.name')
64
+
65
+ if (commandName && (allowedCommands[commandName] || allowedCommands[parent])) return
66
+
67
+ return object.name === 'cy'
68
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'disallow using `async`/`await` in Cypress `before` methods',
8
+ category: 'Possible Errors',
9
+ recommended: true,
10
+ url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-async-before.md',
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ unexpected: 'Avoid using async functions with Cypress before / beforeEach functions',
15
+ },
16
+ },
17
+
18
+ create(context) {
19
+ function isBeforeBlock(callExpressionNode) {
20
+ const { type, name } = callExpressionNode.callee
21
+
22
+ return type === 'Identifier'
23
+ && (name === 'before' || name === 'beforeEach')
24
+ }
25
+
26
+ function isBeforeAsync(node) {
27
+ return node.arguments
28
+ && node.arguments.length >= 2
29
+ && node.arguments[1].async === true
30
+ }
31
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
32
+
33
+ return {
34
+ Identifier(node) {
35
+ if (node.name === 'cy' || node.name === 'Cypress') {
36
+ const ancestors = sourceCode.getAncestors
37
+ ? sourceCode.getAncestors(node)
38
+ : context.getAncestors()
39
+ const asyncTestBlocks = ancestors
40
+ .filter((n) => n.type === 'CallExpression')
41
+ .filter(isBeforeBlock)
42
+ .filter(isBeforeAsync)
43
+
44
+ if (asyncTestBlocks.length >= 1) {
45
+ asyncTestBlocks.forEach((node) => {
46
+ context.report({ node, messageId: 'unexpected' })
47
+ })
48
+ }
49
+ }
50
+ },
51
+ }
52
+ },
53
+ }
@@ -0,0 +1,53 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'disallow using `async`/`await` in Cypress test cases',
8
+ category: 'Possible Errors',
9
+ recommended: true,
10
+ url: 'https://github.com/cypress-io/eslint-plugin-cypress/blob/master/docs/rules/no-async-tests.md',
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ unexpected: 'Avoid using async functions with Cypress tests',
15
+ },
16
+ },
17
+
18
+ create(context) {
19
+ function isTestBlock(callExpressionNode) {
20
+ const { type, name } = callExpressionNode.callee
21
+
22
+ return type === 'Identifier'
23
+ && (name === 'it' || name === 'test')
24
+ }
25
+
26
+ function isTestAsync(node) {
27
+ return node.arguments
28
+ && node.arguments.length >= 2
29
+ && node.arguments[1].async === true
30
+ }
31
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
32
+
33
+ return {
34
+ Identifier(node) {
35
+ if (node.name === 'cy' || node.name === 'Cypress') {
36
+ const ancestors = sourceCode.getAncestors
37
+ ? sourceCode.getAncestors(node)
38
+ : context.getAncestors()
39
+ const asyncTestBlocks = ancestors
40
+ .filter((n) => n.type === 'CallExpression')
41
+ .filter(isTestBlock)
42
+ .filter(isTestAsync)
43
+
44
+ if (asyncTestBlocks.length >= 1) {
45
+ asyncTestBlocks.forEach((node) => {
46
+ context.report({ node, messageId: 'unexpected' })
47
+ })
48
+ }
49
+ }
50
+ },
51
+ }
52
+ },
53
+ }