prefer-user-event.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.MAPPING_TO_USER_EVENT = exports.UserEventMethods = exports.RULE_NAME = void 0;
  4. const utils_1 = require("@typescript-eslint/utils");
  5. const create_testing_library_rule_1 = require("../create-testing-library-rule");
  6. const node_utils_1 = require("../node-utils");
  7. exports.RULE_NAME = 'prefer-user-event';
  8. exports.UserEventMethods = [
  9. 'click',
  10. 'dblClick',
  11. 'type',
  12. 'upload',
  13. 'clear',
  14. 'selectOptions',
  15. 'deselectOptions',
  16. 'tab',
  17. 'hover',
  18. 'unhover',
  19. 'paste',
  20. ];
  21. exports.MAPPING_TO_USER_EVENT = {
  22. click: ['click', 'type', 'selectOptions', 'deselectOptions'],
  23. change: ['upload', 'type', 'clear', 'selectOptions', 'deselectOptions'],
  24. dblClick: ['dblClick'],
  25. input: ['type', 'upload', 'selectOptions', 'deselectOptions', 'paste'],
  26. keyDown: ['type', 'tab'],
  27. keyPress: ['type'],
  28. keyUp: ['type', 'tab'],
  29. mouseDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
  30. mouseEnter: ['hover', 'selectOptions', 'deselectOptions'],
  31. mouseLeave: ['unhover'],
  32. mouseMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'],
  33. mouseOut: ['unhover'],
  34. mouseOver: ['hover', 'selectOptions', 'deselectOptions'],
  35. mouseUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
  36. paste: ['paste'],
  37. pointerDown: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
  38. pointerEnter: ['hover', 'selectOptions', 'deselectOptions'],
  39. pointerLeave: ['unhover'],
  40. pointerMove: ['hover', 'unhover', 'selectOptions', 'deselectOptions'],
  41. pointerOut: ['unhover'],
  42. pointerOver: ['hover', 'selectOptions', 'deselectOptions'],
  43. pointerUp: ['click', 'dblClick', 'selectOptions', 'deselectOptions'],
  44. };
  45. function buildErrorMessage(fireEventMethod) {
  46. const userEventMethods = exports.MAPPING_TO_USER_EVENT[fireEventMethod].map((methodName) => `userEvent.${methodName}`);
  47. return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1');
  48. }
  49. const fireEventMappedMethods = Object.keys(exports.MAPPING_TO_USER_EVENT);
  50. exports.default = (0, create_testing_library_rule_1.createTestingLibraryRule)({
  51. name: exports.RULE_NAME,
  52. meta: {
  53. type: 'suggestion',
  54. docs: {
  55. description: 'Suggest using `userEvent` over `fireEvent` for simulating user interactions',
  56. recommendedConfig: {
  57. dom: false,
  58. angular: false,
  59. react: false,
  60. vue: false,
  61. marko: false,
  62. },
  63. },
  64. messages: {
  65. preferUserEvent: 'Prefer using {{userEventMethods}} over fireEvent.{{fireEventMethod}}',
  66. },
  67. schema: [
  68. {
  69. type: 'object',
  70. properties: {
  71. allowedMethods: { type: 'array' },
  72. },
  73. },
  74. ],
  75. },
  76. defaultOptions: [{ allowedMethods: [] }],
  77. create(context, [options], helpers) {
  78. const { allowedMethods } = options;
  79. const createEventVariables = {};
  80. const isfireEventMethodAllowed = (methodName) => !fireEventMappedMethods.includes(methodName) ||
  81. allowedMethods.includes(methodName);
  82. const getFireEventMethodName = (callExpressionNode, node) => {
  83. if (!utils_1.ASTUtils.isIdentifier(callExpressionNode.callee) &&
  84. !(0, node_utils_1.isMemberExpression)(callExpressionNode.callee)) {
  85. return node.name;
  86. }
  87. const secondArgument = callExpressionNode.arguments[1];
  88. if (utils_1.ASTUtils.isIdentifier(secondArgument) &&
  89. createEventVariables[secondArgument.name] !== undefined) {
  90. return createEventVariables[secondArgument.name];
  91. }
  92. if (!(0, node_utils_1.isCallExpression)(secondArgument) ||
  93. !helpers.isCreateEventUtil(secondArgument)) {
  94. return node.name;
  95. }
  96. if (utils_1.ASTUtils.isIdentifier(secondArgument.callee)) {
  97. return secondArgument.arguments[0]
  98. .value;
  99. }
  100. return secondArgument.callee
  101. .property.name;
  102. };
  103. return {
  104. 'CallExpression Identifier'(node) {
  105. if (!helpers.isFireEventMethod(node)) {
  106. return;
  107. }
  108. const closestCallExpression = (0, node_utils_1.findClosestCallExpressionNode)(node, true);
  109. if (!closestCallExpression) {
  110. return;
  111. }
  112. const fireEventMethodName = getFireEventMethodName(closestCallExpression, node);
  113. if (!fireEventMethodName ||
  114. isfireEventMethodAllowed(fireEventMethodName)) {
  115. return;
  116. }
  117. context.report({
  118. node: closestCallExpression.callee,
  119. messageId: 'preferUserEvent',
  120. data: {
  121. userEventMethods: buildErrorMessage(fireEventMethodName),
  122. fireEventMethod: fireEventMethodName,
  123. },
  124. });
  125. },
  126. VariableDeclarator(node) {
  127. if (!(0, node_utils_1.isCallExpression)(node.init) ||
  128. !helpers.isCreateEventUtil(node.init) ||
  129. !utils_1.ASTUtils.isIdentifier(node.id)) {
  130. return;
  131. }
  132. let fireEventMethodName = '';
  133. if ((0, node_utils_1.isMemberExpression)(node.init.callee) &&
  134. utils_1.ASTUtils.isIdentifier(node.init.callee.property)) {
  135. fireEventMethodName = node.init.callee.property.name;
  136. }
  137. else if (node.init.arguments.length > 0) {
  138. fireEventMethodName = node.init.arguments[0]
  139. .value;
  140. }
  141. if (!isfireEventMethodAllowed(fireEventMethodName)) {
  142. createEventVariables[node.id.name] = fireEventMethodName;
  143. }
  144. },
  145. };
  146. },
  147. });