valid-title.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _experimentalUtils = require("@typescript-eslint/experimental-utils");
  7. var _utils = require("./utils");
  8. const trimFXprefix = word => ['f', 'x'].includes(word.charAt(0)) ? word.substr(1) : word;
  9. const doesBinaryExpressionContainStringNode = binaryExp => {
  10. if ((0, _utils.isStringNode)(binaryExp.right)) {
  11. return true;
  12. }
  13. if (binaryExp.left.type === _experimentalUtils.AST_NODE_TYPES.BinaryExpression) {
  14. return doesBinaryExpressionContainStringNode(binaryExp.left);
  15. }
  16. return (0, _utils.isStringNode)(binaryExp.left);
  17. };
  18. const quoteStringValue = node => node.type === _experimentalUtils.AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw;
  19. const compileMatcherPatterns = matchers => {
  20. if (typeof matchers === 'string') {
  21. const matcher = new RegExp(matchers, 'u');
  22. return {
  23. describe: matcher,
  24. test: matcher,
  25. it: matcher
  26. };
  27. }
  28. return {
  29. describe: matchers.describe ? new RegExp(matchers.describe, 'u') : null,
  30. test: matchers.test ? new RegExp(matchers.test, 'u') : null,
  31. it: matchers.it ? new RegExp(matchers.it, 'u') : null
  32. };
  33. };
  34. var _default = (0, _utils.createRule)({
  35. name: __filename,
  36. meta: {
  37. docs: {
  38. category: 'Best Practices',
  39. description: 'Enforce valid titles',
  40. recommended: 'error'
  41. },
  42. messages: {
  43. titleMustBeString: 'Title must be a string',
  44. emptyTitle: '{{ jestFunctionName }} should not have an empty title',
  45. duplicatePrefix: 'should not have duplicate prefix',
  46. accidentalSpace: 'should not have leading or trailing spaces',
  47. disallowedWord: '"{{ word }}" is not allowed in test titles.',
  48. mustNotMatch: '{{ jestFunctionName }} should not match {{ pattern }}',
  49. mustMatch: '{{ jestFunctionName }} should match {{ pattern }}'
  50. },
  51. type: 'suggestion',
  52. schema: [{
  53. type: 'object',
  54. properties: {
  55. ignoreTypeOfDescribeName: {
  56. type: 'boolean',
  57. default: false
  58. },
  59. disallowedWords: {
  60. type: 'array',
  61. items: {
  62. type: 'string'
  63. }
  64. },
  65. mustNotMatch: {
  66. oneOf: [{
  67. type: 'string'
  68. }, {
  69. type: 'object',
  70. properties: {
  71. describe: {
  72. type: 'string'
  73. },
  74. test: {
  75. type: 'string'
  76. },
  77. it: {
  78. type: 'string'
  79. }
  80. },
  81. additionalProperties: false
  82. }]
  83. },
  84. mustMatch: {
  85. oneOf: [{
  86. type: 'string'
  87. }, {
  88. type: 'object',
  89. properties: {
  90. describe: {
  91. type: 'string'
  92. },
  93. test: {
  94. type: 'string'
  95. },
  96. it: {
  97. type: 'string'
  98. }
  99. },
  100. additionalProperties: false
  101. }]
  102. }
  103. },
  104. additionalProperties: false
  105. }],
  106. fixable: 'code'
  107. },
  108. defaultOptions: [{
  109. ignoreTypeOfDescribeName: false,
  110. disallowedWords: []
  111. }],
  112. create(context, [{
  113. ignoreTypeOfDescribeName,
  114. disallowedWords = [],
  115. mustNotMatch,
  116. mustMatch
  117. }]) {
  118. const disallowedWordsRegexp = new RegExp(`\\b(${disallowedWords.join('|')})\\b`, 'iu');
  119. const mustNotMatchPatterns = compileMatcherPatterns(mustNotMatch !== null && mustNotMatch !== void 0 ? mustNotMatch : {});
  120. const mustMatchPatterns = compileMatcherPatterns(mustMatch !== null && mustMatch !== void 0 ? mustMatch : {});
  121. return {
  122. CallExpression(node) {
  123. if (!(0, _utils.isDescribeCall)(node) && !(0, _utils.isTestCaseCall)(node)) {
  124. return;
  125. }
  126. const [argument] = node.arguments;
  127. if (!argument) {
  128. return;
  129. }
  130. if (!(0, _utils.isStringNode)(argument)) {
  131. if (argument.type === _experimentalUtils.AST_NODE_TYPES.BinaryExpression && doesBinaryExpressionContainStringNode(argument)) {
  132. return;
  133. }
  134. if (argument.type !== _experimentalUtils.AST_NODE_TYPES.TemplateLiteral && !(ignoreTypeOfDescribeName && (0, _utils.isDescribeCall)(node))) {
  135. context.report({
  136. messageId: 'titleMustBeString',
  137. loc: argument.loc
  138. });
  139. }
  140. return;
  141. }
  142. const title = (0, _utils.getStringValue)(argument);
  143. if (!title) {
  144. context.report({
  145. messageId: 'emptyTitle',
  146. data: {
  147. jestFunctionName: (0, _utils.isDescribeCall)(node) ? _utils.DescribeAlias.describe : _utils.TestCaseName.test
  148. },
  149. node
  150. });
  151. return;
  152. }
  153. if (disallowedWords.length > 0) {
  154. const disallowedMatch = disallowedWordsRegexp.exec(title);
  155. if (disallowedMatch) {
  156. context.report({
  157. data: {
  158. word: disallowedMatch[1]
  159. },
  160. messageId: 'disallowedWord',
  161. node: argument
  162. });
  163. return;
  164. }
  165. }
  166. if (title.trim().length !== title.length) {
  167. context.report({
  168. messageId: 'accidentalSpace',
  169. node: argument,
  170. fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]) +?/u, '$1').replace(/ +?([`'"])$/u, '$1'))]
  171. });
  172. }
  173. const nodeName = trimFXprefix((0, _utils.getNodeName)(node));
  174. const [firstWord] = title.split(' ');
  175. if (firstWord.toLowerCase() === nodeName) {
  176. context.report({
  177. messageId: 'duplicatePrefix',
  178. node: argument,
  179. fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]).+? /u, '$1'))]
  180. });
  181. }
  182. const [jestFunctionName] = nodeName.split('.');
  183. const mustNotMatchPattern = mustNotMatchPatterns[jestFunctionName];
  184. if (mustNotMatchPattern) {
  185. if (mustNotMatchPattern.test(title)) {
  186. context.report({
  187. messageId: 'mustNotMatch',
  188. node: argument,
  189. data: {
  190. jestFunctionName,
  191. pattern: mustNotMatchPattern
  192. }
  193. });
  194. return;
  195. }
  196. }
  197. const mustMatchPattern = mustMatchPatterns[jestFunctionName];
  198. if (mustMatchPattern) {
  199. if (!mustMatchPattern.test(title)) {
  200. context.report({
  201. messageId: 'mustMatch',
  202. node: argument,
  203. data: {
  204. jestFunctionName,
  205. pattern: mustMatchPattern
  206. }
  207. });
  208. return;
  209. }
  210. }
  211. }
  212. };
  213. }
  214. });
  215. exports.default = _default;