-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add 'assertion-before-screenshot' rule
- Loading branch information
Showing
6 changed files
with
185 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
], | ||
}) |