rules.test.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. const { RuleTester } = require('eslint')
  2. const preferKebabUrl = require('./prefer-kebab-url')
  3. const noUnnecessaryTrans = require('./no-unnecessary-trans')
  4. const shouldUnescapeTrans = require('./should-unescape-trans')
  5. const noGeneratedEditorThemes = require('./no-generated-editor-themes')
  6. const viDoMockValidPath = require('./require-vi-doMock-valid-path')
  7. const requireCioSnakeCaseProperties = require('./require-cio-snake-case-properties')
  8. const ruleTester = new RuleTester({
  9. parser: require.resolve('@typescript-eslint/parser'),
  10. parserOptions: {
  11. ecmaVersion: 'latest',
  12. ecmaFeatures: { jsx: true },
  13. },
  14. })
  15. ruleTester.run('prefer-kebab-url', preferKebabUrl, {
  16. valid: [
  17. { code: `app.get('/foo-bar')` },
  18. { code: `app.get('/foo-bar/:id')` },
  19. { code: `router.post('/foo-bar')` },
  20. { code: `router.get('/foo-bar/:id/:name/:age')` },
  21. { code: `webRouter.get('/foo-bar/:user_id/(ProjectName)/get-info')` },
  22. { code: `webApp.post('/foo-bar/:user_id/(ProjectName)/get-info')` },
  23. {
  24. code: `router.get(/^\\/download\\/project\\/([^/]*)\\/output\\/output\\.pdf$/)`,
  25. },
  26. {
  27. code: `webRouter.get(/^\\/project\\/([^/]*)\\/user\\/([0-9a-f]+)\\/build\\/([0-9a-f-]+)\\/output\\/(.*)$/)`,
  28. },
  29. ],
  30. invalid: [
  31. {
  32. code: `app.get('/fooBar')`,
  33. errors: [{ message: 'Route path should be in kebab-case.' }],
  34. },
  35. {
  36. code: `app.get('/fooBar/:id')`,
  37. errors: [{ message: 'Route path should be in kebab-case.' }],
  38. },
  39. {
  40. code: `webRouter.get('/foo_bar/:id/FooBar/:name/fooBar')`,
  41. errors: [{ message: 'Route path should be in kebab-case.' }],
  42. },
  43. {
  44. code: `router.get(/^\\/downLoad\\/pro-ject\\/([^/]*)\\/OutPut\\/out-put\\.pdf$/)`,
  45. errors: [{ message: 'Route path should be in kebab-case.' }],
  46. },
  47. ],
  48. })
  49. ruleTester.run('no-unnecessary-trans', noUnnecessaryTrans, {
  50. valid: [
  51. { code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>` },
  52. ],
  53. invalid: [
  54. {
  55. code: `<Trans i18nKey="test" values={{ test: 'foo '}}/>`,
  56. errors: [{ message: `Use t('…') when there are no components` }],
  57. },
  58. {
  59. code: `<Trans i18nKey="test" />`,
  60. errors: [{ message: `Use t('…') when there are no components` }],
  61. output: `{t('test')}`,
  62. },
  63. ],
  64. })
  65. ruleTester.run('should-unescape-trans', shouldUnescapeTrans, {
  66. valid: [
  67. {
  68. code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>`,
  69. },
  70. {
  71. code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape tOptions={{ interpolation: { escapeValue: true } }}/>`,
  72. },
  73. ],
  74. invalid: [
  75. {
  76. code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} />`,
  77. errors: [{ message: 'Trans with values must have shouldUnescape' }],
  78. output: `<Trans i18nKey="test" values={{ foo: 'bar' }}\nshouldUnescape components={{ strong: <strong/> }} />`,
  79. },
  80. {
  81. code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape />`,
  82. errors: [
  83. {
  84. message:
  85. 'Trans with shouldUnescape must have tOptions.interpolation.escapeValue',
  86. },
  87. ],
  88. output: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape\ntOptions={{ interpolation: { escapeValue: true } }} />`,
  89. },
  90. ],
  91. })
  92. const noGeneratedEditorThemesError =
  93. 'EditorView.theme and EditorView.baseTheme each add CSS to the page for every instance of the theme. Store the theme in a variable and reuse it instead.'
  94. ruleTester.run('no-generated-editor-themes', noGeneratedEditorThemes, {
  95. valid: [
  96. {
  97. code: `EditorView.theme({ '.cm-editor': { color: 'black' } })`,
  98. },
  99. {
  100. code: `const theme = EditorView.theme({ '.cm-editor': { color: 'black' } })`,
  101. },
  102. ],
  103. invalid: [
  104. {
  105. code: `function createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) }`,
  106. errors: [
  107. {
  108. message: noGeneratedEditorThemesError,
  109. },
  110. ],
  111. },
  112. {
  113. code: `() => EditorView.theme({ '.cm-editor': { color: 'black' } })`,
  114. errors: [
  115. {
  116. message: noGeneratedEditorThemesError,
  117. },
  118. ],
  119. },
  120. {
  121. code: `class Foo { createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) } }`,
  122. errors: [
  123. {
  124. message: noGeneratedEditorThemesError,
  125. },
  126. ],
  127. },
  128. ],
  129. })
  130. ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
  131. valid: [
  132. {
  133. code: 'vi.doMock("./require-vi-doMock-valid-path.js")',
  134. filename: __filename,
  135. },
  136. {
  137. code: 'const filename = "./require-vi-doMock-valid-path.js"; vi.doMock(filename);',
  138. filename: __filename,
  139. },
  140. ],
  141. invalid: [
  142. {
  143. code: "vi.doMock('./require-vi-doMock-valid-path2')",
  144. filename: __filename,
  145. errors: [
  146. {
  147. message:
  148. 'The path "./require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.',
  149. },
  150. ],
  151. },
  152. {
  153. code: 'const filename = "./require-vi-doMock-valid-path2.js"; vi.doMock(filename);',
  154. filename: __filename,
  155. errors: [
  156. {
  157. message:
  158. 'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
  159. },
  160. ],
  161. },
  162. ],
  163. })
  164. ruleTester.run(
  165. 'require-cio-snake-case-properties',
  166. requireCioSnakeCaseProperties,
  167. {
  168. valid: [
  169. // updateUserAttributes with snake_case keys
  170. {
  171. code: `CustomerIoHandler.updateUserAttributes(userId, { plan_type: 'free', group_size: 10 })`,
  172. },
  173. // Modules.promises.hooks.fire with snake_case keys
  174. {
  175. code: `Modules.promises.hooks.fire('setUserProperties', userId, { plan_type: 'free', last_active: 123 })`,
  176. },
  177. // Modules.hooks.fire with snake_case keys
  178. {
  179. code: `Modules.hooks.fire('setUserProperties', userId, { plan_type: 'free' })`,
  180. },
  181. // Single-word keys are valid snake_case
  182. {
  183. code: `CustomerIoHandler.updateUserAttributes(userId, { email: 'a@b.com', role: 'admin' })`,
  184. },
  185. // Computed/dynamic keys are skipped
  186. {
  187. code: `CustomerIoHandler.updateUserAttributes(userId, { [dynamicKey]: true })`,
  188. },
  189. // Spread elements are skipped
  190. {
  191. code: `CustomerIoHandler.updateUserAttributes(userId, { ...existingAttrs })`,
  192. },
  193. // Unrelated function calls are not checked
  194. {
  195. code: `SomeOtherHandler.updateUserAttributes(userId, { camelCase: true })`,
  196. },
  197. // fire() with a different event name is not checked
  198. {
  199. code: `Modules.promises.hooks.fire('someOtherEvent', userId, { camelCase: true })`,
  200. },
  201. ],
  202. invalid: [
  203. // camelCase key in updateUserAttributes
  204. {
  205. code: `CustomerIoHandler.updateUserAttributes(userId, { planType: 'free' })`,
  206. errors: [
  207. {
  208. message: `Customer.io attribute 'planType' must be in snake_case.`,
  209. },
  210. ],
  211. },
  212. // kebab-case string key
  213. {
  214. code: `CustomerIoHandler.updateUserAttributes(userId, { 'plan-type': 'free' })`,
  215. errors: [
  216. {
  217. message: `Customer.io attribute 'plan-type' must be in snake_case.`,
  218. },
  219. ],
  220. },
  221. // PascalCase key
  222. {
  223. code: `CustomerIoHandler.updateUserAttributes(userId, { PlanType: 'free' })`,
  224. errors: [
  225. {
  226. message: `Customer.io attribute 'PlanType' must be in snake_case.`,
  227. },
  228. ],
  229. },
  230. // camelCase in Modules.promises.hooks.fire
  231. {
  232. code: `Modules.promises.hooks.fire('setUserProperties', userId, { planType: 'free' })`,
  233. errors: [
  234. {
  235. message: `Customer.io attribute 'planType' must be in snake_case.`,
  236. },
  237. ],
  238. },
  239. // camelCase in Modules.hooks.fire
  240. {
  241. code: `Modules.hooks.fire('setUserProperties', userId, { planType: 'free' })`,
  242. errors: [
  243. {
  244. message: `Customer.io attribute 'planType' must be in snake_case.`,
  245. },
  246. ],
  247. },
  248. // Multiple invalid keys report multiple errors
  249. {
  250. code: `CustomerIoHandler.updateUserAttributes(userId, { planType: 'free', groupSize: 10, plan_term: 'annual' })`,
  251. errors: [
  252. {
  253. message: `Customer.io attribute 'planType' must be in snake_case.`,
  254. },
  255. {
  256. message: `Customer.io attribute 'groupSize' must be in snake_case.`,
  257. },
  258. ],
  259. },
  260. ],
  261. }
  262. )