Skip to content

Commit

Permalink
feat: Add 'assertion-before-screenshot' rule
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbreiding committed Dec 28, 2018
2 parents 8c561bb + 0b1b4b6 commit e5f0504
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -49,6 +49,7 @@ Rules with a check mark (✅) are enabled by default while using the `plugin:cyp
|:---|:--------|:------------|
|| [no-assigning-return-values](./docs/rules/no-assigning-return-values.md) | Prevent assigning return values of cy calls |
|| [no-unnecessary-waiting](./docs/rules/no-unnecessary-waiting.md) | Prevent waiting for arbitrary time periods |
| | [assertion-before-screenshot](./docs/rules/assertion-before-screenshot.md) | Ensure screenshots are preceded by an assertion |

## Chai and `no-unused-expressions`

Expand Down
22 changes: 22 additions & 0 deletions docs/rules/assertion-before-screenshot.md
@@ -0,0 +1,22 @@
## Assertion Before Screenshot

If you take screenshots without assertions then you may get different screenshots depending on timing.

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.

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.

Invalid:

```
cy.visit('myUrl');
cy.screenshot();
```

Valid:

```
cy.visit('myUrl');
cy.get('[data-test-id="my-element"]').should('be.visible');
cy.screenshot();
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -4,6 +4,7 @@ module.exports = {
rules: {
'no-assigning-return-values': require('./lib/rules/no-assigning-return-values'),
'no-unnecessary-waiting': require('./lib/rules/no-unnecessary-waiting'),
'assertion-before-screenshot': require('./lib/rules/assertion-before-screenshot'),
},
configs: {
recommended: require('./lib/config/recommended'),
Expand Down
110 changes: 110 additions & 0 deletions lib/rules/assertion-before-screenshot.js
@@ -0,0 +1,110 @@
/**
* @fileoverview Assert on the page state before taking a screenshot, so the screenshot is consistent
* @author Luke Page
*/

'use strict'

const assertionCommands = [
// assertions
'should',
'and',
'contains',

// retries until it gets something
'get',

// not an assertion, but unlikely to require waiting for render
'scrollIntoView',
'scrollTo',
];

module.exports = {
meta: {
docs: {
description: 'Assert on the page state before taking a screenshot, so the screenshot is consistent',
category: 'Possible Errors',
recommended: false,
},
schema: [],
messages: {
unexpected: 'Make an assertion on the page state before taking a screenshot',
},
},
create (context) {
return {
CallExpression (node) {
if (isCallingCyScreenshot(node) && !isPreviousAnAssertion(node)) {
context.report({ node, messageId: 'unexpected' })
}
},
}
},
}

function isRootCypress(node) {
while(node.type === 'CallExpression') {
if (node.callee.type !== 'MemberExpression') return false
if (node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'cy') {
return true
}
node = node.callee.object
}
return false
}

function getPreviousInChain(node) {
return node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'CallExpression' &&
node.callee.object.callee.type === 'MemberExpression' &&
node.callee.object.callee.property.type === 'Identifier' &&
node.callee.object.callee.property.name
}

function getCallExpressionCypressCommand(node) {
return isRootCypress(node) &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name
}

function isCallingCyScreenshot (node) {
return getCallExpressionCypressCommand(node) === 'screenshot'
}

function getPreviousCypressCommand(node) {
const previousInChain = getPreviousInChain(node)

if (previousInChain) {
return previousInChain
}

while(node.parent && !node.parent.body) {
node = node.parent
}

if (!node.parent || !node.parent.body) return null

const body = node.parent.body.type === 'BlockStatement' ? node.parent.body.body : node.parent.body

const index = body.indexOf(node)

// in the case of a function declaration it won't be found
if (index < 0) return null

if (index === 0) return getPreviousCypressCommand(node.parent);

const previousStatement = body[index - 1]

if (previousStatement.type !== 'ExpressionStatement' ||
previousStatement.expression.type !== 'CallExpression')
return null

return getCallExpressionCypressCommand(previousStatement.expression)
}

function isPreviousAnAssertion (node) {
const previousCypressCommand = getPreviousCypressCommand(node)
return assertionCommands.indexOf(previousCypressCommand) >= 0
}
22 changes: 16 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions tests/lib/rules/assertion-before-screenshot.js
@@ -0,0 +1,35 @@
'use strict'

const rule = require('../../../lib/rules/assertion-before-screenshot')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester()

const errors = [{ messageId: 'unexpected' }]
const parserOptions = { ecmaVersion: 6 }

ruleTester.run('assertion-before-screenshot', rule, {
valid: [
{ code: 'cy.get(".some-element"); cy.screenshot();', parserOptions },
{ code: 'cy.get(".some-element").should("exist").screenshot();', parserOptions },
{ code: 'cy.get(".some-element").should("exist").screenshot().click()', parserOptions, errors },
{ code: 'cy.get(".some-element").should("exist"); if(true) cy.screenshot();', parserOptions },
{ code: 'if(true) { cy.get(".some-element").should("exist"); cy.screenshot(); }', parserOptions },
{ code: 'cy.get(".some-element").should("exist"); if(true) { cy.screenshot(); }', parserOptions },
{ code: 'const a = () => { cy.get(".some-element").should("exist"); cy.screenshot(); }', parserOptions, errors },
{ code: 'cy.get(".some-element").should("exist").and("be.visible"); cy.screenshot();', parserOptions },
{ code: 'cy.get(".some-element").contains("Text"); cy.screenshot();', parserOptions },
],

invalid: [
{ code: 'cy.screenshot()', parserOptions, errors },
{ code: 'cy.visit("somepage"); cy.screenshot();', parserOptions, errors },
{ code: 'cy.custom(); cy.screenshot()', parserOptions, errors },
{ code: 'cy.get(".some-element").click(); cy.screenshot()', parserOptions, errors },
{ code: 'cy.get(".some-element").click().screenshot()', parserOptions, errors },
{ code: 'if(true) { cy.get(".some-element").click(); cy.screenshot(); }', parserOptions, errors },
{ code: 'cy.get(".some-element").click(); if(true) { cy.screenshot(); }', parserOptions, errors },
{ code: 'cy.get(".some-element"); function a() { cy.screenshot(); }', parserOptions, errors },
{ code: 'cy.get(".some-element"); const a = () => { cy.screenshot(); }', parserOptions, errors },
],
})

0 comments on commit e5f0504

Please sign in to comment.