From 8628331ff8028c1f61e143097a8e3a42318b417c Mon Sep 17 00:00:00 2001 From: Rick Fleuren Date: Fri, 12 Apr 2024 17:19:08 +0200 Subject: [PATCH] Feat: add linting rule for before/beforeEach just like the async test rule (#151) * Feat: add linting rule for before/beforeEach just like the async rule for test * Fix the unit test * Fix the unit test * Update docs/rules/no-async-before.md Co-authored-by: Bill Glesias * Update docs/rules/no-async-before.md Co-authored-by: Bill Glesias --------- Co-authored-by: Bill Glesias --- README.md | 1 + docs/rules/no-async-before.md | 52 ++++++++++++++++++++++++++++++ lib/rules/no-async-before.js | 47 +++++++++++++++++++++++++++ tests/lib/rules/no-async-before.js | 25 ++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 docs/rules/no-async-before.md create mode 100644 lib/rules/no-async-before.js create mode 100644 tests/lib/rules/no-async-before.js diff --git a/README.md b/README.md index a8bcf219..54c11b76 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ You can add rules: "cypress/assertion-before-screenshot": "warn", "cypress/no-force": "warn", "cypress/no-async-tests": "error", + "cypress/no-async-before": "error", "cypress/no-pause": "error" } } diff --git a/docs/rules/no-async-before.md b/docs/rules/no-async-before.md new file mode 100644 index 00000000..8acd26b4 --- /dev/null +++ b/docs/rules/no-async-before.md @@ -0,0 +1,52 @@ +# Prevent using async/await in Cypress test cases (no-async-tests) + +Cypress commands that return a promise may cause side effects in before/beforeEach hooks, possibly causing unexpected behavior. + +## Rule Details + +This rule disallows using `async` `before` and `beforeEach` functions. + +Examples of **incorrect** code for this rule: + +```js +describe('my feature', () => { + before('my test case', async () => { + await cy.get('.myClass') + // other operations + }) +}) +``` + +```js +describe('my feature', () => { + before('my test case', async () => { + cy + .get('.myClass') + .click() + + await someAsyncFunction() + }) +}) +``` + +Examples of **correct** code for this rule: + +```js +describe('my feature', () => { + before('my test case', () => { + cy.get('.myClass') + // other operations + }) +}) + +``` + +## When Not To Use It + +If there are genuine use-cases for using `async/await` in your before then you may not want to include this rule (or at least demote it to a warning). + +## Further Reading + +- [Commands Are Asynchronous](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous) +- [Commands Are Promises](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Promises) +- [Commands Are Not Promises](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Not-Promises) diff --git a/lib/rules/no-async-before.js b/lib/rules/no-async-before.js new file mode 100644 index 00000000..d8fe6350 --- /dev/null +++ b/lib/rules/no-async-before.js @@ -0,0 +1,47 @@ +'use strict' + +module.exports = { + meta: { + docs: { + description: 'Prevent using async/await in Cypress before methods', + category: 'Possible Errors', + recommended: true, + }, + messages: { + unexpected: 'Avoid using async functions with Cypress before / beforeEach functions', + }, + }, + + create (context) { + function isBeforeBlock (callExpressionNode) { + const { type, name } = callExpressionNode.callee + + return type === 'Identifier' + && name === 'before' || name === 'beforeEach' + } + + function isBeforeAsync (node) { + return node.arguments + && node.arguments.length >= 2 + && node.arguments[1].async === true + } + + return { + Identifier (node) { + if (node.name === 'cy' || node.name === 'Cypress') { + const ancestors = context.getAncestors() + const asyncTestBlocks = ancestors + .filter((n) => n.type === 'CallExpression') + .filter(isBeforeBlock) + .filter(isBeforeAsync) + + if (asyncTestBlocks.length >= 1) { + asyncTestBlocks.forEach((node) => { + context.report({ node, messageId: 'unexpected' }) + }) + } + } + }, + } + }, +} diff --git a/tests/lib/rules/no-async-before.js b/tests/lib/rules/no-async-before.js new file mode 100644 index 00000000..6c485319 --- /dev/null +++ b/tests/lib/rules/no-async-before.js @@ -0,0 +1,25 @@ +'use strict' + +const rule = require('../../../lib/rules/no-async-before') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester() + +const errors = [{ messageId: 'unexpected' }] +// async functions are an ES2017 feature +const parserOptions = { ecmaVersion: 8 } + +ruleTester.run('no-async-before', rule, { + valid: [ + { code: 'before(\'a before case\', () => { cy.get(\'.someClass\'); })', parserOptions }, + { code: 'before(\'a before case\', async () => { await somethingAsync(); })', parserOptions }, + { code: 'async function nonTestFn () { return await somethingAsync(); }', parserOptions }, + { code: 'const nonTestArrowFn = async () => { await somethingAsync(); }', parserOptions }, + ], + invalid: [ + { code: 'before(\'a test case\', async () => { cy.get(\'.someClass\'); })', parserOptions, errors }, + { code: 'beforeEach(\'a test case\', async () => { cy.get(\'.someClass\'); })', parserOptions, errors }, + { code: 'before(\'a test case\', async function () { cy.get(\'.someClass\'); })', parserOptions, errors }, + { code: 'beforeEach(\'a test case\', async function () { cy.get(\'.someClass\'); })', parserOptions, errors }, + ], +})