eslint-plugin-cypress 2.1.1 → 2.2.1

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/README.md CHANGED
@@ -21,9 +21,6 @@ Add an `.eslintrc.json` file to your `cypress` directory with the following:
21
21
  "plugins": [
22
22
  "cypress"
23
23
  ],
24
- "extends": [
25
- "plugin:cypress/recommended"
26
- ],
27
24
  "env": {
28
25
  "cypress/globals": true
29
26
  }
@@ -32,21 +29,27 @@ Add an `.eslintrc.json` file to your `cypress` directory with the following:
32
29
 
33
30
  ## Rules
34
31
 
35
- Rules are grouped by category to help you understand their purpose.
32
+ These rules enforce some of the [best practices recommended for using Cypress](https://on.cypress.io/best-practices). Use them by adding the following to your eslint config:
36
33
 
37
- Rules with a check mark (✅) are enabled by default while using
38
- the `plugin:cypress/recommended` config.
34
+ ```json
35
+ // my-project/cypress/.eslintrc.json
39
36
 
40
- The --fix option on the command line automatically fixes problems reported by
41
- rules which have a wrench (🔧) below.
37
+ {
38
+ "extends": [
39
+ "plugin:cypress/recommended"
40
+ ]
41
+ }
42
+ ```
42
43
 
44
+ Rules with a check mark (✅) are enabled by default while using the `plugin:cypress/recommended` config.
43
45
 
44
- ### Possible Errors
46
+ **NOTE**: These rules currently require eslint 5.0 or greater. If you would like support added for eslint 4.x, please 👍 [this issue](https://github.com/cypress-io/eslint-plugin-cypress/issues/14).
45
47
 
46
48
  | | Rule ID | Description |
47
49
  |:---|:--------|:------------|
48
50
  | ✅ | [no-assigning-return-values](./docs/rules/no-assigning-return-values.md) | Prevent assigning return values of cy calls |
49
51
  | ✅ | [no-unnecessary-waiting](./docs/rules/no-unnecessary-waiting.md) | Prevent waiting for arbitrary time periods |
52
+ | | [assertion-before-screenshot](./docs/rules/assertion-before-screenshot.md) | Ensure screenshots are preceded by an assertion |
50
53
 
51
54
  ## Chai and `no-unused-expressions`
52
55
 
@@ -0,0 +1,22 @@
1
+ ## Assertion Before Screenshot
2
+
3
+ If you take screenshots without assertions then you may get different screenshots depending on timing.
4
+
5
+ For example, if clicking a button makes some network calls and upon success, renders something, then the screenshot may sometimes have the new render and sometimes not.
6
+
7
+ This rule checks there is an assertion making sure your application state is correct before doing a screenshot. This makes sure the result of the screenshot will be consistent.
8
+
9
+ Invalid:
10
+
11
+ ```
12
+ cy.visit('myUrl');
13
+ cy.screenshot();
14
+ ```
15
+
16
+ Valid:
17
+
18
+ ```
19
+ cy.visit('myUrl');
20
+ cy.get('[data-test-id="my-element"]').should('be.visible');
21
+ cy.screenshot();
22
+ ```
@@ -1,3 +1,3 @@
1
1
  ## No Assigning Return Values
2
2
 
3
- See [the Cypress Best Practices guide](https://docs.cypress.io/guides/references/best-practices.html#Assigning-Return-Values).
3
+ See [the Cypress Best Practices guide](https://on.cypress.io/best-practices#Assigning-Return-Values).
@@ -1,3 +1,3 @@
1
1
  ## No Assigning Return Values
2
2
 
3
- See [the Cypress Best Practices guide](https://docs.cypress.io/guides/references/best-practices.html#Unnecessary-Waiting).
3
+ See [the Cypress Best Practices guide](https://on.cypress.io/best-practices#Unnecessary-Waiting).
package/index.js CHANGED
@@ -4,6 +4,7 @@ module.exports = {
4
4
  rules: {
5
5
  'no-assigning-return-values': require('./lib/rules/no-assigning-return-values'),
6
6
  'no-unnecessary-waiting': require('./lib/rules/no-unnecessary-waiting'),
7
+ 'assertion-before-screenshot': require('./lib/rules/assertion-before-screenshot'),
7
8
  },
8
9
  configs: {
9
10
  recommended: require('./lib/config/recommended'),
@@ -15,6 +16,7 @@ module.exports = {
15
16
  Cypress: false,
16
17
  expect: false,
17
18
  assert: false,
19
+ chai: false,
18
20
  }),
19
21
  parserOptions: {
20
22
  ecmaVersion: 2017,
@@ -1,9 +1,9 @@
1
- 'use strict';
1
+ 'use strict'
2
2
 
3
3
  module.exports = {
4
4
  plugins: ['cypress'],
5
5
  rules: {
6
6
  'cypress/no-assigning-return-values': 'error',
7
- 'cypress/no-unnecessary-waiting': 'error'
8
- }
9
- };
7
+ 'cypress/no-unnecessary-waiting': 'error',
8
+ },
9
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @fileoverview Assert on the page state before taking a screenshot, so the screenshot is consistent
3
+ * @author Luke Page
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const assertionCommands = [
9
+ // assertions
10
+ 'should',
11
+ 'and',
12
+ 'contains',
13
+
14
+ // retries until it gets something
15
+ 'get',
16
+
17
+ // not an assertion, but unlikely to require waiting for render
18
+ 'scrollIntoView',
19
+ 'scrollTo',
20
+ ];
21
+
22
+ module.exports = {
23
+ meta: {
24
+ docs: {
25
+ description: 'Assert on the page state before taking a screenshot, so the screenshot is consistent',
26
+ category: 'Possible Errors',
27
+ recommended: false,
28
+ },
29
+ schema: [],
30
+ messages: {
31
+ unexpected: 'Make an assertion on the page state before taking a screenshot',
32
+ },
33
+ },
34
+ create (context) {
35
+ return {
36
+ CallExpression (node) {
37
+ if (isCallingCyScreenshot(node) && !isPreviousAnAssertion(node)) {
38
+ context.report({ node, messageId: 'unexpected' })
39
+ }
40
+ },
41
+ }
42
+ },
43
+ }
44
+
45
+ function isRootCypress(node) {
46
+ while(node.type === 'CallExpression') {
47
+ if (node.callee.type !== 'MemberExpression') return false
48
+ if (node.callee.object.type === 'Identifier' &&
49
+ node.callee.object.name === 'cy') {
50
+ return true
51
+ }
52
+ node = node.callee.object
53
+ }
54
+ return false
55
+ }
56
+
57
+ function getPreviousInChain(node) {
58
+ return node.type === 'CallExpression' &&
59
+ node.callee.type === 'MemberExpression' &&
60
+ node.callee.object.type === 'CallExpression' &&
61
+ node.callee.object.callee.type === 'MemberExpression' &&
62
+ node.callee.object.callee.property.type === 'Identifier' &&
63
+ node.callee.object.callee.property.name
64
+ }
65
+
66
+ function getCallExpressionCypressCommand(node) {
67
+ return isRootCypress(node) &&
68
+ node.callee.property.type === 'Identifier' &&
69
+ node.callee.property.name
70
+ }
71
+
72
+ function isCallingCyScreenshot (node) {
73
+ return getCallExpressionCypressCommand(node) === 'screenshot'
74
+ }
75
+
76
+ function getPreviousCypressCommand(node) {
77
+ const previousInChain = getPreviousInChain(node)
78
+
79
+ if (previousInChain) {
80
+ return previousInChain
81
+ }
82
+
83
+ while(node.parent && !node.parent.body) {
84
+ node = node.parent
85
+ }
86
+
87
+ if (!node.parent || !node.parent.body) return null
88
+
89
+ const body = node.parent.body.type === 'BlockStatement' ? node.parent.body.body : node.parent.body
90
+
91
+ const index = body.indexOf(node)
92
+
93
+ // in the case of a function declaration it won't be found
94
+ if (index < 0) return null
95
+
96
+ if (index === 0) return getPreviousCypressCommand(node.parent);
97
+
98
+ const previousStatement = body[index - 1]
99
+
100
+ if (previousStatement.type !== 'ExpressionStatement' ||
101
+ previousStatement.expression.type !== 'CallExpression')
102
+ return null
103
+
104
+ return getCallExpressionCypressCommand(previousStatement.expression)
105
+ }
106
+
107
+ function isPreviousAnAssertion (node) {
108
+ const previousCypressCommand = getPreviousCypressCommand(node)
109
+ return assertionCommands.indexOf(previousCypressCommand) >= 0
110
+ }
@@ -3,7 +3,18 @@
3
3
  * @author Elad Shahar
4
4
  */
5
5
 
6
- 'use strict';
6
+ 'use strict'
7
+
8
+ // safely get nested object property
9
+ function get (obj, propertyString = '') {
10
+ const properties = propertyString.split('.')
11
+ for (let i = 0; i < properties.length; i++) {
12
+ const value = (obj || {})[properties[i]]
13
+ if (value == null) return value
14
+ obj = value
15
+ }
16
+ return obj
17
+ }
7
18
 
8
19
  module.exports = {
9
20
  meta: {
@@ -11,36 +22,45 @@ module.exports = {
11
22
  description: 'Prevent assigning return values of cy calls',
12
23
  category: 'Possible Errors',
13
24
  recommended: true,
14
- url: 'https://docs.cypress.io/guides/references/best-practices.html#Assigning-Return-Values'
25
+ url: 'https://on.cypress.io/best-practices#Assigning-Return-Values',
15
26
  },
16
27
  schema: [],
17
28
  messages: {
18
- unexpected: 'Do not assign the return value of a Cypress command'
19
- }
29
+ unexpected: 'Do not assign the return value of a Cypress command',
30
+ },
20
31
  },
21
- create(context) {
32
+ create (context) {
22
33
  return {
23
- VariableDeclaration(node) {
34
+ VariableDeclaration (node) {
24
35
  if (node.declarations.some(isCypressCommandDeclaration)) {
25
- context.report({ node, messageId: 'unexpected' });
36
+ context.report({ node, messageId: 'unexpected' })
26
37
  }
27
- }
28
- };
29
- }
30
- };
38
+ },
39
+ }
40
+ },
41
+ }
31
42
 
32
- function isCypressCommandDeclaration(declarator) {
33
- if (!declarator) { return; }
34
- if (!declarator.init) { return; }
35
- if (!declarator.init.callee) { return; }
43
+ const whitelistedCommands = {
44
+ now: true,
45
+ spy: true,
46
+ state: true,
47
+ stub: true,
48
+ }
36
49
 
37
- let object = declarator.init.callee.object;
50
+ function isCypressCommandDeclaration (declarator) {
51
+ let object = get(declarator, 'init.callee.object')
38
52
 
39
- if (!object) { return; }
53
+ if (!object) return
40
54
 
41
55
  while (object.callee) {
42
- object = object.callee.object;
56
+ object = object.callee.object
57
+
58
+ if (!object) return
43
59
  }
44
60
 
45
- return object.name === 'cy';
61
+ const commandName = get(object, 'parent.property.name')
62
+
63
+ if (commandName && whitelistedCommands[commandName]) return
64
+
65
+ return object.name === 'cy'
46
66
  }
@@ -3,7 +3,7 @@
3
3
  * @author Elad Shahar
4
4
  */
5
5
 
6
- 'use strict';
6
+ 'use strict'
7
7
 
8
8
  module.exports = {
9
9
  meta: {
@@ -11,34 +11,34 @@ module.exports = {
11
11
  description: 'Prevent waiting for arbitrary time periods',
12
12
  category: 'Possible Errors',
13
13
  recommended: true,
14
- url: 'https://docs.cypress.io/guides/references/best-practices.html#Unnecessary-Waiting'
14
+ url: 'https://on.cypress.io/best-practices#Unnecessary-Waiting',
15
15
  },
16
16
  schema: [],
17
17
  messages: {
18
- unexpected: 'Do not wait for arbitrary time periods'
19
- }
18
+ unexpected: 'Do not wait for arbitrary time periods',
19
+ },
20
20
  },
21
- create(context) {
21
+ create (context) {
22
22
  return {
23
- CallExpression(node) {
23
+ CallExpression (node) {
24
24
  if (isCallingCyWait(node) && isNumberArgument(node)) {
25
- context.report({ node, messageId: 'unexpected' });
25
+ context.report({ node, messageId: 'unexpected' })
26
26
  }
27
- }
28
- };
29
- }
30
- };
27
+ },
28
+ }
29
+ },
30
+ }
31
31
 
32
- function isCallingCyWait(node) {
32
+ function isCallingCyWait (node) {
33
33
  return node.callee.type === 'MemberExpression' &&
34
34
  node.callee.object.type === 'Identifier' &&
35
35
  node.callee.object.name === 'cy' &&
36
36
  node.callee.property.type === 'Identifier' &&
37
- node.callee.property.name === 'wait';
37
+ node.callee.property.name === 'wait'
38
38
  }
39
39
 
40
- function isNumberArgument(node) {
40
+ function isNumberArgument (node) {
41
41
  return node.arguments.length > 0 &&
42
42
  node.arguments[0].type === 'Literal' &&
43
- typeof(node.arguments[0].value) === 'number';
43
+ typeof (node.arguments[0].value) === 'number'
44
44
  }
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"eslint-plugin-cypress","version":"2.1.1","description":"An ESLint plugin for projects using Cypress","main":"index.js","author":"Chris Breiding (chris@cypress.io)","license":"MIT","keywords":["eslint","eslintplugin","cypress"],"repository":{"type":"git","url":"git+https://github.com/cypress-io/eslint-plugin-cypress.git"},"bugs":{"url":"https://github.com/cypress-io/eslint-plugin-cypress/issues"},"homepage":"https://github.com/cypress-io/eslint-plugin-cypress#readme","peerDependencies":{"eslint":">= 3.2.1"},"dependencies":{"globals":"^11.0.1"},"devDependencies":{"condition-circle":"1.5.0","eslint":"^5.7.0","eslint-plugin-cypress-dev":"1.1.2","github-post-release":"1.13.1","husky":"^0.14.3","jest":"^23.6.0","semantic-release":"8.2.0","simple-commit-message":"3.3.1"},"scripts":{"precommit":"npm run lint","lint":"eslint *.js","semantic-release":"semantic-release pre && npm publish --access public && semantic-release post","start":"yarn run test:watch","test":"jest","test-watch":"jest --watchAll"},"release":{"verifyConditions":"condition-circle","analyzeCommits":"simple-commit-message","generateNotes":"github-post-release"},"jest":{"testMatch":["**/tests/**/*.js"]}}
1
+ {"name":"eslint-plugin-cypress","version":"2.2.1","description":"An ESLint plugin for projects using Cypress","main":"index.js","author":"Chris Breiding (chris@cypress.io)","license":"MIT","keywords":["eslint","eslintplugin","cypress"],"repository":{"type":"git","url":"git+https://github.com/cypress-io/eslint-plugin-cypress.git"},"bugs":{"url":"https://github.com/cypress-io/eslint-plugin-cypress/issues"},"homepage":"https://github.com/cypress-io/eslint-plugin-cypress#readme","peerDependencies":{"eslint":">= 3.2.1"},"dependencies":{"globals":"^11.0.1"},"devDependencies":{"condition-circle":"1.5.0","eslint":"^5.7.0","eslint-plugin-cypress-dev":"1.1.2","github-post-release":"1.13.1","husky":"^0.14.3","jest":"^23.6.0","semantic-release":"8.2.0","simple-commit-message":"3.3.1"},"scripts":{"precommit":"npm run lint","lint":"eslint *.js","semantic-release":"semantic-release pre && npm publish --access public && semantic-release post","start":"yarn run test:watch","test":"jest","test-watch":"jest --watchAll"},"release":{"verifyConditions":"condition-circle","analyzeCommits":"simple-commit-message","generateNotes":"github-post-release"},"jest":{"testMatch":["**/tests/**/*.js"]}}
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const rule = require('../../../lib/rules/assertion-before-screenshot')
4
+ const RuleTester = require('eslint').RuleTester
5
+
6
+ const ruleTester = new RuleTester()
7
+
8
+ const errors = [{ messageId: 'unexpected' }]
9
+ const parserOptions = { ecmaVersion: 6 }
10
+
11
+ ruleTester.run('assertion-before-screenshot', rule, {
12
+ valid: [
13
+ { code: 'cy.get(".some-element"); cy.screenshot();', parserOptions },
14
+ { code: 'cy.get(".some-element").should("exist").screenshot();', parserOptions },
15
+ { code: 'cy.get(".some-element").should("exist").screenshot().click()', parserOptions, errors },
16
+ { code: 'cy.get(".some-element").should("exist"); if(true) cy.screenshot();', parserOptions },
17
+ { code: 'if(true) { cy.get(".some-element").should("exist"); cy.screenshot(); }', parserOptions },
18
+ { code: 'cy.get(".some-element").should("exist"); if(true) { cy.screenshot(); }', parserOptions },
19
+ { code: 'const a = () => { cy.get(".some-element").should("exist"); cy.screenshot(); }', parserOptions, errors },
20
+ { code: 'cy.get(".some-element").should("exist").and("be.visible"); cy.screenshot();', parserOptions },
21
+ { code: 'cy.get(".some-element").contains("Text"); cy.screenshot();', parserOptions },
22
+ ],
23
+
24
+ invalid: [
25
+ { code: 'cy.screenshot()', parserOptions, errors },
26
+ { code: 'cy.visit("somepage"); cy.screenshot();', parserOptions, errors },
27
+ { code: 'cy.custom(); cy.screenshot()', parserOptions, errors },
28
+ { code: 'cy.get(".some-element").click(); cy.screenshot()', parserOptions, errors },
29
+ { code: 'cy.get(".some-element").click().screenshot()', parserOptions, errors },
30
+ { code: 'if(true) { cy.get(".some-element").click(); cy.screenshot(); }', parserOptions, errors },
31
+ { code: 'cy.get(".some-element").click(); if(true) { cy.screenshot(); }', parserOptions, errors },
32
+ { code: 'cy.get(".some-element"); function a() { cy.screenshot(); }', parserOptions, errors },
33
+ { code: 'cy.get(".some-element"); const a = () => { cy.screenshot(); }', parserOptions, errors },
34
+ ],
35
+ })
@@ -1,12 +1,12 @@
1
- "use strict";
1
+ 'use strict'
2
2
 
3
- const rule = require('../../../lib/rules/no-assigning-return-values');
4
- const RuleTester = require('eslint').RuleTester;
3
+ const rule = require('../../../lib/rules/no-assigning-return-values')
4
+ const RuleTester = require('eslint').RuleTester
5
5
 
6
- const ruleTester = new RuleTester();
6
+ const ruleTester = new RuleTester()
7
7
 
8
- const errors = [{ messageId: 'unexpected' }];
9
- const parserOptions = { ecmaVersion: 6 };
8
+ const errors = [{ messageId: 'unexpected' }]
9
+ const parserOptions = { ecmaVersion: 6 }
10
10
 
11
11
  ruleTester.run('no-assigning-return-values', rule, {
12
12
  valid: [
@@ -14,8 +14,13 @@ ruleTester.run('no-assigning-return-values', rule, {
14
14
  { code: 'let foo = true;', parserOptions },
15
15
  { code: 'const foo = true;', parserOptions },
16
16
  { code: 'const foo = bar();', parserOptions },
17
+ { code: 'const foo = bar().baz();', parserOptions },
18
+ { code: 'const spy = cy.spy();', parserOptions },
19
+ { code: 'const stub = cy.stub();', parserOptions },
20
+ { code: 'const result = cy.now();', parserOptions },
21
+ { code: 'const state = cy.state();', parserOptions },
17
22
  { code: 'cy.get("foo");', parserOptions },
18
- { code: 'cy.contains("foo").click();', parserOptions }
23
+ { code: 'cy.contains("foo").click();', parserOptions },
19
24
  ],
20
25
 
21
26
  invalid: [
@@ -27,6 +32,6 @@ ruleTester.run('no-assigning-return-values', rule, {
27
32
  { code: 'let a = cy.window()', parserOptions, errors },
28
33
  { code: 'let a = cy.wait("@something")', parserOptions, errors },
29
34
 
30
- { code: 'let a = cy.contains("foo").click()', parserOptions, errors }
31
- ]
32
- });
35
+ { code: 'let a = cy.contains("foo").click()', parserOptions, errors },
36
+ ],
37
+ })
@@ -1,12 +1,12 @@
1
- "use strict";
1
+ 'use strict'
2
2
 
3
- const rule = require('../../../lib/rules/no-unnecessary-waiting');
4
- const RuleTester = require('eslint').RuleTester;
3
+ const rule = require('../../../lib/rules/no-unnecessary-waiting')
4
+ const RuleTester = require('eslint').RuleTester
5
5
 
6
- const ruleTester = new RuleTester();
6
+ const ruleTester = new RuleTester()
7
7
 
8
- const errors = [{ messageId: 'unexpected' }];
9
- const parserOptions = { ecmaVersion: 6 };
8
+ const errors = [{ messageId: 'unexpected' }]
9
+ const parserOptions = { ecmaVersion: 6 }
10
10
 
11
11
  ruleTester.run('no-unnecessary-waiting', rule, {
12
12
  valid: [
@@ -15,14 +15,14 @@ ruleTester.run('no-unnecessary-waiting', rule, {
15
15
  { code: 'cy.wait("@someRequest").then((xhr) => xhr)', parserOptions },
16
16
  { code: 'cy.wait(["@someRequest", "@anotherRequest"])', parserOptions },
17
17
 
18
- { code: 'cy.clock(5000)', parserOptions},
18
+ { code: 'cy.clock(5000)', parserOptions },
19
19
  { code: 'cy.scrollTo(0, 10)', parserOptions },
20
- { code: 'cy.tick(500)', parserOptions }
20
+ { code: 'cy.tick(500)', parserOptions },
21
21
  ],
22
22
 
23
23
  invalid: [
24
24
  { code: 'cy.wait(0)', parserOptions, errors },
25
25
  { code: 'cy.wait(100)', parserOptions, errors },
26
- { code: 'cy.wait(5000)', parserOptions, errors }
27
- ]
28
- });
26
+ { code: 'cy.wait(5000)', parserOptions, errors },
27
+ ],
28
+ })