await-interactions.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. "use strict";
  2. /**
  3. * @fileoverview Interactions should be awaited
  4. * @author Yann Braga
  5. */
  6. const create_storybook_rule_1 = require("../utils/create-storybook-rule");
  7. const constants_1 = require("../utils/constants");
  8. const ast_1 = require("../utils/ast");
  9. module.exports = (0, create_storybook_rule_1.createStorybookRule)({
  10. name: 'await-interactions',
  11. defaultOptions: [],
  12. meta: {
  13. docs: {
  14. description: 'Interactions should be awaited',
  15. categories: [constants_1.CategoryId.ADDON_INTERACTIONS, constants_1.CategoryId.RECOMMENDED],
  16. recommended: 'error', // or 'warn'
  17. },
  18. messages: {
  19. interactionShouldBeAwaited: 'Interaction should be awaited: {{method}}',
  20. fixSuggestion: 'Add `await` to method',
  21. },
  22. type: 'problem',
  23. fixable: 'code',
  24. hasSuggestions: true,
  25. schema: [],
  26. },
  27. create(context) {
  28. // variables should be defined here
  29. //----------------------------------------------------------------------
  30. // Helpers
  31. //----------------------------------------------------------------------
  32. // any helper functions should go here or else delete this section
  33. const FUNCTIONS_TO_BE_AWAITED = [
  34. 'waitFor',
  35. 'waitForElementToBeRemoved',
  36. 'wait',
  37. 'waitForElement',
  38. 'waitForDomChange',
  39. 'userEvent',
  40. 'play',
  41. ];
  42. const getMethodThatShouldBeAwaited = (expr) => {
  43. const shouldAwait = (name) => {
  44. return FUNCTIONS_TO_BE_AWAITED.includes(name) || name.startsWith('findBy');
  45. };
  46. // When an expression is a return value it doesn't need to be awaited
  47. if ((0, ast_1.isArrowFunctionExpression)(expr.parent) || (0, ast_1.isReturnStatement)(expr.parent)) {
  48. return null;
  49. }
  50. if ((0, ast_1.isMemberExpression)(expr.callee) &&
  51. (0, ast_1.isIdentifier)(expr.callee.object) &&
  52. shouldAwait(expr.callee.object.name)) {
  53. return expr.callee.object;
  54. }
  55. if ((0, ast_1.isTSNonNullExpression)(expr.callee) &&
  56. (0, ast_1.isMemberExpression)(expr.callee.expression) &&
  57. (0, ast_1.isIdentifier)(expr.callee.expression.property) &&
  58. shouldAwait(expr.callee.expression.property.name)) {
  59. return expr.callee.expression.property;
  60. }
  61. if ((0, ast_1.isMemberExpression)(expr.callee) &&
  62. (0, ast_1.isIdentifier)(expr.callee.property) &&
  63. shouldAwait(expr.callee.property.name)) {
  64. return expr.callee.property;
  65. }
  66. if ((0, ast_1.isMemberExpression)(expr.callee) &&
  67. (0, ast_1.isCallExpression)(expr.callee.object) &&
  68. (0, ast_1.isIdentifier)(expr.callee.object.callee) &&
  69. (0, ast_1.isIdentifier)(expr.callee.property) &&
  70. expr.callee.object.callee.name === 'expect') {
  71. return expr.callee.property;
  72. }
  73. if ((0, ast_1.isIdentifier)(expr.callee) && shouldAwait(expr.callee.name)) {
  74. return expr.callee;
  75. }
  76. return null;
  77. };
  78. const getClosestFunctionAncestor = (node) => {
  79. const parent = node.parent;
  80. if (!parent || (0, ast_1.isProgram)(parent))
  81. return undefined;
  82. if ((0, ast_1.isArrowFunctionExpression)(parent) ||
  83. (0, ast_1.isFunctionExpression)(parent) ||
  84. (0, ast_1.isFunctionDeclaration)(parent)) {
  85. return node.parent;
  86. }
  87. return getClosestFunctionAncestor(parent);
  88. };
  89. const isUserEventFromStorybookImported = (node) => {
  90. return (node.source.value === '@storybook/testing-library' &&
  91. node.specifiers.find((spec) => (0, ast_1.isImportSpecifier)(spec) &&
  92. spec.imported.name === 'userEvent' &&
  93. spec.local.name === 'userEvent') !== undefined);
  94. };
  95. const isExpectFromStorybookImported = (node) => {
  96. return (node.source.value === '@storybook/jest' &&
  97. node.specifiers.find((spec) => (0, ast_1.isImportSpecifier)(spec) && spec.imported.name === 'expect') !== undefined);
  98. };
  99. //----------------------------------------------------------------------
  100. // Public
  101. //----------------------------------------------------------------------
  102. /**
  103. * @param {import('eslint').Rule.Node} node
  104. */
  105. let isImportedFromStorybook = true;
  106. const invocationsThatShouldBeAwaited = [];
  107. return {
  108. ImportDeclaration(node) {
  109. isImportedFromStorybook =
  110. isUserEventFromStorybookImported(node) || isExpectFromStorybookImported(node);
  111. },
  112. VariableDeclarator(node) {
  113. isImportedFromStorybook =
  114. isImportedFromStorybook && (0, ast_1.isIdentifier)(node.id) && node.id.name !== 'userEvent';
  115. },
  116. CallExpression(node) {
  117. var _a;
  118. const method = getMethodThatShouldBeAwaited(node);
  119. if (method && !(0, ast_1.isAwaitExpression)(node.parent) && !(0, ast_1.isAwaitExpression)((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent)) {
  120. invocationsThatShouldBeAwaited.push({ node, method });
  121. }
  122. },
  123. 'Program:exit': function () {
  124. if (isImportedFromStorybook && invocationsThatShouldBeAwaited.length) {
  125. invocationsThatShouldBeAwaited.forEach(({ node, method }) => {
  126. const parentFnNode = getClosestFunctionAncestor(node);
  127. const parentFnNeedsAsync = parentFnNode && !('async' in parentFnNode && parentFnNode.async);
  128. const fixFn = (fixer) => {
  129. const fixerResult = [fixer.insertTextBefore(node, 'await ')];
  130. if (parentFnNeedsAsync) {
  131. fixerResult.push(fixer.insertTextBefore(parentFnNode, 'async '));
  132. }
  133. return fixerResult;
  134. };
  135. context.report({
  136. node,
  137. messageId: 'interactionShouldBeAwaited',
  138. data: {
  139. method: method.name,
  140. },
  141. fix: fixFn,
  142. suggest: [
  143. {
  144. messageId: 'fixSuggestion',
  145. fix: fixFn,
  146. },
  147. ],
  148. });
  149. });
  150. }
  151. },
  152. };
  153. },
  154. });