From cbd8cffcbe69e869e8e7b07cc5a0af95e769e9b0 Mon Sep 17 00:00:00 2001 From: Rick Fleuren Date: Fri, 1 Mar 2024 10:30:54 +0100 Subject: [PATCH 1/5] Feat: add linting rule for before/beforeEach just like the async rule for test --- 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..69c7ee54 --- /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 before that return a promise will cause tests to prematurely start, can cause unexpected behavior. An `async` function returns a promise under the hood, so a before using an `async` function might also error. + +## 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..55fc3065 --- /dev/null +++ b/tests/lib/rules/no-async-before.js @@ -0,0 +1,25 @@ +'use strict' + +const rule = require('../../../lib/rules/no-async-tests') +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-tests', 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 }, + ], +}) From aea3cef923e22a0851abf69a6262af617c26466b Mon Sep 17 00:00:00 2001 From: Rick Fleuren Date: Fri, 1 Mar 2024 10:33:54 +0100 Subject: [PATCH 2/5] Fix the unit test --- tests/lib/rules/no-async-before.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/no-async-before.js b/tests/lib/rules/no-async-before.js index 55fc3065..0add9330 100644 --- a/tests/lib/rules/no-async-before.js +++ b/tests/lib/rules/no-async-before.js @@ -1,6 +1,6 @@ 'use strict' -const rule = require('../../../lib/rules/no-async-tests') +const rule = require('../../../lib/rules/no-async-before') const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester() From cb6093d98cfb5f13d4c896da88b0956f5838a855 Mon Sep 17 00:00:00 2001 From: Rick Fleuren Date: Fri, 1 Mar 2024 10:34:10 +0100 Subject: [PATCH 3/5] Fix the unit test --- tests/lib/rules/no-async-before.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/no-async-before.js b/tests/lib/rules/no-async-before.js index 0add9330..6c485319 100644 --- a/tests/lib/rules/no-async-before.js +++ b/tests/lib/rules/no-async-before.js @@ -9,7 +9,7 @@ const errors = [{ messageId: 'unexpected' }] // async functions are an ES2017 feature const parserOptions = { ecmaVersion: 8 } -ruleTester.run('no-async-tests', rule, { +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 }, From fbc99a4755370eab7f9acd1b683fd0a3b3bad9dc Mon Sep 17 00:00:00 2001 From: Rick Fleuren Date: Wed, 3 Apr 2024 08:38:24 +0200 Subject: [PATCH 4/5] Update docs/rules/no-async-before.md Co-authored-by: Bill Glesias --- docs/rules/no-async-before.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-async-before.md b/docs/rules/no-async-before.md index 69c7ee54..6de715ff 100644 --- a/docs/rules/no-async-before.md +++ b/docs/rules/no-async-before.md @@ -4,7 +4,7 @@ Cypress before that return a promise will cause tests to prematurely start, can ## Rule Details -This rule disallows using `async` before and beforeEach functions. +This rule disallows using `async` `before` and `beforeEach` functions. Examples of **incorrect** code for this rule: From b0772e1012bdaa9aaab6100ba8c9b1d6e3e0ba3b Mon Sep 17 00:00:00 2001 From: Rick Fleuren Date: Wed, 3 Apr 2024 08:38:48 +0200 Subject: [PATCH 5/5] Update docs/rules/no-async-before.md Co-authored-by: Bill Glesias --- docs/rules/no-async-before.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-async-before.md b/docs/rules/no-async-before.md index 6de715ff..8acd26b4 100644 --- a/docs/rules/no-async-before.md +++ b/docs/rules/no-async-before.md @@ -1,6 +1,6 @@ # Prevent using async/await in Cypress test cases (no-async-tests) -Cypress before that return a promise will cause tests to prematurely start, can cause unexpected behavior. An `async` function returns a promise under the hood, so a before using an `async` function might also error. +Cypress commands that return a promise may cause side effects in before/beforeEach hooks, possibly causing unexpected behavior. ## Rule Details