unsafe-to-chain-command.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. 'use strict'
  2. const { basename } = require('path')
  3. const NAME = basename(__dirname)
  4. const DESCRIPTION = 'Actions should be in the end of chains, not in the middle'
  5. /**
  6. * Commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx.'
  7. * See {@link https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle Actions should be at the end of chains, not the middle}
  8. * for more information.
  9. *
  10. * @type {string[]}
  11. */
  12. const unsafeToChainActions = [
  13. 'blur',
  14. 'clear',
  15. 'click',
  16. 'check',
  17. 'dblclick',
  18. 'each',
  19. 'focus$',
  20. 'rightclick',
  21. 'screenshot',
  22. 'scrollIntoView',
  23. 'scrollTo',
  24. 'select',
  25. 'selectFile',
  26. 'spread',
  27. 'submit',
  28. 'type',
  29. 'trigger',
  30. 'uncheck',
  31. 'within',
  32. ]
  33. /**
  34. * @type {import('eslint').Rule.RuleMetaData['schema']}
  35. */
  36. const schema = {
  37. title: NAME,
  38. description: DESCRIPTION,
  39. type: 'object',
  40. properties: {
  41. methods: {
  42. type: 'array',
  43. description:
  44. 'An additional list of methods to check for unsafe chaining.',
  45. default: [],
  46. },
  47. },
  48. }
  49. /**
  50. * @param {import('eslint').Rule.RuleContext} context
  51. * @returns {Record<string, any>}
  52. */
  53. const getDefaultOptions = (context) => {
  54. return Object.entries(schema.properties).reduce((acc, [key, value]) => {
  55. if (!(value.default in value)) return acc
  56. return {
  57. ...acc,
  58. [key]: value.default,
  59. }
  60. }, context.options[0] || {})
  61. }
  62. /** @type {import('eslint').Rule.RuleModule} */
  63. module.exports = {
  64. meta: {
  65. docs: {
  66. description: DESCRIPTION,
  67. category: 'Possible Errors',
  68. recommended: true,
  69. url: 'https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle',
  70. },
  71. schema: [schema],
  72. messages: {
  73. unexpected:
  74. '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.',
  75. },
  76. },
  77. create (context) {
  78. const { methods } = getDefaultOptions(context)
  79. return {
  80. CallExpression (node) {
  81. if (
  82. isRootCypress(node) &&
  83. isActionUnsafeToChain(node, methods) &&
  84. node.parent.type === 'MemberExpression'
  85. ) {
  86. context.report({
  87. node,
  88. messageId: 'unexpected',
  89. })
  90. }
  91. },
  92. }
  93. },
  94. }
  95. /**
  96. * @param {import('estree').Node} node
  97. * @returns {boolean}
  98. */
  99. const isRootCypress = (node) => {
  100. if (
  101. node.type !== 'CallExpression' ||
  102. node.callee.type !== 'MemberExpression'
  103. ) {
  104. return false
  105. }
  106. if (
  107. node.callee.object.type === 'Identifier' &&
  108. node.callee.object.name === 'cy'
  109. ) {
  110. return true
  111. }
  112. return isRootCypress(node.callee.object)
  113. }
  114. /**
  115. * @param {import('estree').Node} node
  116. * @param {(string | RegExp)[]} additionalMethods
  117. */
  118. const isActionUnsafeToChain = (node, additionalMethods = []) => {
  119. const unsafeActionsRegex = new RegExp([
  120. ...unsafeToChainActions,
  121. ...additionalMethods.map((method) => method instanceof RegExp ? method.source : method),
  122. ].join('|'))
  123. return (
  124. node.callee &&
  125. node.callee.property &&
  126. node.callee.property.type === 'Identifier' &&
  127. unsafeActionsRegex.test(node.callee.property.name)
  128. )
  129. }