diff --git a/.github/workflows/add-issue-triage-board.yml b/.github/workflows/add-issue-triage-board.yml new file mode 100644 index 00000000..e9e74b93 --- /dev/null +++ b/.github/workflows/add-issue-triage-board.yml @@ -0,0 +1,12 @@ +name: 'Add issue/PR to Triage Board' +on: + issues: + types: + - opened + pull_request_target: + types: + - opened +jobs: + add-to-triage-project-board: + uses: cypress-io/cypress/.github/workflows/triage_add_to_project.yml@develop + secrets: inherit diff --git a/.github/workflows/triage_closed_issue_comment.yml b/.github/workflows/triage_closed_issue_comment.yml new file mode 100644 index 00000000..0061e2fe --- /dev/null +++ b/.github/workflows/triage_closed_issue_comment.yml @@ -0,0 +1,9 @@ +name: 'Handle Comment Workflow' +on: + issue_comment: + types: + - created +jobs: + closed-issue-comment: + uses: cypress-io/cypress/.github/workflows/triage_handle_new_comments.yml@develop + secrets: inherit diff --git a/README.md b/README.md index 4ad2b5e6..a8bcf219 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Disable the `cypress/no-unnecessary-waiting` rule for the entire file by placing /* eslint-disable cypress/no-unnecessary-waiting */ ``` -Disable the `cypress/no-unnecessary-waiting` rule for a portion of the file: +Disable the `cypress/no-unnecessary-waiting` rule for only a portion of the file: ```js it('waits for a second', () => { @@ -121,6 +121,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 | | ✅ | [no-async-tests](./docs/rules/no-async-tests.md) | Prevent using async/await in Cypress test case | +| ✅ | [unsafe-to-chain-command](./docs/rules/unsafe-to-chain-command.md) | Prevent chaining from unsafe to chain commands | | | [no-force](./docs/rules/no-force.md) | Disallow using `force: true` with action commands | | | [assertion-before-screenshot](./docs/rules/assertion-before-screenshot.md) | Ensure screenshots are preceded by an assertion | | | [require-data-selectors](./docs/rules/require-data-selectors.md) | Only allow data-\* attribute selectors (require-data-selectors) | diff --git a/docs/rules/no-unnecessary-waiting.md b/docs/rules/no-unnecessary-waiting.md index 5a05d408..b291c3ea 100644 --- a/docs/rules/no-unnecessary-waiting.md +++ b/docs/rules/no-unnecessary-waiting.md @@ -1,3 +1,3 @@ -## No Assigning Return Values +## No Unnecessary Waiting See [the Cypress Best Practices guide](https://on.cypress.io/best-practices#Unnecessary-Waiting). diff --git a/docs/rules/unsafe-to-chain-command.md b/docs/rules/unsafe-to-chain-command.md new file mode 100644 index 00000000..5186175b --- /dev/null +++ b/docs/rules/unsafe-to-chain-command.md @@ -0,0 +1,3 @@ +## Unsafe to chain command + +See [retry-ability guide](https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle). diff --git a/index.js b/index.js index 76a53172..8d49e57e 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const globals = require('globals') module.exports = { rules: { 'no-assigning-return-values': require('./lib/rules/no-assigning-return-values'), + 'unsafe-to-chain-command': require('./lib/rules/unsafe-to-chain-command'), 'no-unnecessary-waiting': require('./lib/rules/no-unnecessary-waiting'), 'no-async-tests': require('./lib/rules/no-async-tests'), 'assertion-before-screenshot': require('./lib/rules/assertion-before-screenshot'), diff --git a/lib/config/recommended.js b/lib/config/recommended.js index 1ff0d0f4..a61a2ba7 100644 --- a/lib/config/recommended.js +++ b/lib/config/recommended.js @@ -9,5 +9,6 @@ module.exports = { 'cypress/no-assigning-return-values': 'error', 'cypress/no-unnecessary-waiting': 'error', 'cypress/no-async-tests': 'error', + 'cypress/unsafe-to-chain-command': 'error', }, } diff --git a/lib/rules/unsafe-to-chain-command.js b/lib/rules/unsafe-to-chain-command.js new file mode 100644 index 00000000..490e9585 --- /dev/null +++ b/lib/rules/unsafe-to-chain-command.js @@ -0,0 +1,47 @@ +'use strict' + +module.exports = { + meta: { + docs: { + description: 'Actions should be in the end of chains, not in the middle', + category: 'Possible Errors', + recommended: true, + url: 'https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle', + }, + schema: [], + messages: { + unexpected: 'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.', + }, + }, + create (context) { + return { + CallExpression (node) { + if (isRootCypress(node) && isActionUnsafeToChain(node) && node.parent.type === 'MemberExpression') { + 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 isActionUnsafeToChain (node) { + // commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx' + const unsafeToChainActions = ['blur', 'clear', 'click', 'check', 'dblclick', 'each', 'focus', 'rightclick', 'screenshot', 'scrollIntoView', 'scrollTo', 'select', 'selectFile', 'spread', 'submit', 'type', 'trigger', 'uncheck', 'within'] + + return node.callee && node.callee.property && node.callee.property.type === 'Identifier' && unsafeToChainActions.includes(node.callee.property.name) +} diff --git a/tests/lib/rules/unsafe-to-chain-command.js b/tests/lib/rules/unsafe-to-chain-command.js new file mode 100644 index 00000000..64326faa --- /dev/null +++ b/tests/lib/rules/unsafe-to-chain-command.js @@ -0,0 +1,20 @@ +'use strict' + +const rule = require('../../../lib/rules/unsafe-to-chain-command') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester() + +const errors = [{ messageId: 'unexpected' }] +const parserOptions = { ecmaVersion: 6 } + +ruleTester.run('action-ends-chain', rule, { + valid: [ + { code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");', parserOptions }, + ], + + invalid: [ + { code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");', parserOptions, errors }, + { code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");', parserOptions, errors }, + ], +})