rules.test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. const { RuleTester } = require('eslint')
  2. const tsParser = require('@typescript-eslint/parser')
  3. const noThrowInCallback = require('./no-throw-in-callback')
  4. const preferKebabUrl = require('./prefer-kebab-url')
  5. const noUnnecessaryTrans = require('./no-unnecessary-trans')
  6. const shouldUnescapeTrans = require('./should-unescape-trans')
  7. const noGeneratedEditorThemes = require('./no-generated-editor-themes')
  8. const viDoMockValidPath = require('./require-vi-doMock-valid-path')
  9. const requireCioSnakeCaseProperties = require('./require-cio-snake-case-properties')
  10. const ruleTester = new RuleTester({
  11. languageOptions: {
  12. parser: tsParser,
  13. ecmaVersion: 'latest',
  14. parserOptions: { ecmaFeatures: { jsx: true } },
  15. },
  16. })
  17. ruleTester.run('prefer-kebab-url', preferKebabUrl, {
  18. valid: [
  19. { code: `app.get('/foo-bar')` },
  20. { code: `app.get('/foo-bar/:id')` },
  21. { code: `router.post('/foo-bar')` },
  22. { code: `router.get('/foo-bar/:id/:name/:age')` },
  23. { code: `webRouter.get('/foo-bar/:user_id/(ProjectName)/get-info')` },
  24. { code: `webApp.post('/foo-bar/:user_id/(ProjectName)/get-info')` },
  25. {
  26. code: `router.get(/^\\/download\\/project\\/([^/]*)\\/output\\/output\\.pdf$/)`,
  27. },
  28. {
  29. code: `webRouter.get(/^\\/project\\/([^/]*)\\/user\\/([0-9a-f]+)\\/build\\/([0-9a-f-]+)\\/output\\/(.*)$/)`,
  30. },
  31. ],
  32. invalid: [
  33. {
  34. code: `app.get('/fooBar')`,
  35. errors: [
  36. { message: 'Route path should be in kebab-case.', suggestions: 1 },
  37. ],
  38. },
  39. {
  40. code: `app.get('/fooBar/:id')`,
  41. errors: [
  42. { message: 'Route path should be in kebab-case.', suggestions: 1 },
  43. ],
  44. },
  45. {
  46. code: `webRouter.get('/foo_bar/:id/FooBar/:name/fooBar')`,
  47. errors: [
  48. { message: 'Route path should be in kebab-case.', suggestions: 1 },
  49. ],
  50. },
  51. {
  52. code: `router.get(/^\\/downLoad\\/pro-ject\\/([^/]*)\\/OutPut\\/out-put\\.pdf$/)`,
  53. errors: [
  54. { message: 'Route path should be in kebab-case.', suggestions: 1 },
  55. ],
  56. },
  57. ],
  58. })
  59. ruleTester.run('no-unnecessary-trans', noUnnecessaryTrans, {
  60. valid: [
  61. { code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>` },
  62. ],
  63. invalid: [
  64. {
  65. code: `<Trans i18nKey="test" values={{ test: 'foo '}}/>`,
  66. errors: [{ message: `Use t('…') when there are no components` }],
  67. },
  68. {
  69. code: `<Trans i18nKey="test" />`,
  70. errors: [{ message: `Use t('…') when there are no components` }],
  71. output: `{t('test')}`,
  72. },
  73. ],
  74. })
  75. ruleTester.run('should-unescape-trans', shouldUnescapeTrans, {
  76. valid: [
  77. {
  78. code: `<Trans i18nKey="test" components={{ strong: <strong/> }}/>`,
  79. },
  80. {
  81. code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape tOptions={{ interpolation: { escapeValue: true } }}/>`,
  82. },
  83. ],
  84. invalid: [
  85. {
  86. code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} />`,
  87. errors: [{ message: 'Trans with values must have shouldUnescape' }],
  88. output: `<Trans i18nKey="test" values={{ foo: 'bar' }}\nshouldUnescape components={{ strong: <strong/> }} />`,
  89. },
  90. {
  91. code: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape />`,
  92. errors: [
  93. {
  94. message:
  95. 'Trans with shouldUnescape must have tOptions.interpolation.escapeValue',
  96. },
  97. ],
  98. output: `<Trans i18nKey="test" values={{ foo: 'bar' }} components={{ strong: <strong/> }} shouldUnescape\ntOptions={{ interpolation: { escapeValue: true } }} />`,
  99. },
  100. ],
  101. })
  102. const noGeneratedEditorThemesError =
  103. '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.'
  104. ruleTester.run('no-generated-editor-themes', noGeneratedEditorThemes, {
  105. valid: [
  106. {
  107. code: `EditorView.theme({ '.cm-editor': { color: 'black' } })`,
  108. },
  109. {
  110. code: `const theme = EditorView.theme({ '.cm-editor': { color: 'black' } })`,
  111. },
  112. ],
  113. invalid: [
  114. {
  115. code: `function createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) }`,
  116. errors: [
  117. {
  118. message: noGeneratedEditorThemesError,
  119. },
  120. ],
  121. },
  122. {
  123. code: `() => EditorView.theme({ '.cm-editor': { color: 'black' } })`,
  124. errors: [
  125. {
  126. message: noGeneratedEditorThemesError,
  127. },
  128. ],
  129. },
  130. {
  131. code: `class Foo { createTheme() { return EditorView.theme({ '.cm-editor': { color: 'black' } }) } }`,
  132. errors: [
  133. {
  134. message: noGeneratedEditorThemesError,
  135. },
  136. ],
  137. },
  138. ],
  139. })
  140. ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
  141. valid: [
  142. {
  143. code: 'vi.doMock("./require-vi-doMock-valid-path.js")',
  144. filename: __filename,
  145. },
  146. {
  147. code: 'const filename = "./require-vi-doMock-valid-path.js"; vi.doMock(filename);',
  148. filename: __filename,
  149. },
  150. ],
  151. invalid: [
  152. {
  153. code: "vi.doMock('./require-vi-doMock-valid-path2')",
  154. filename: __filename,
  155. errors: [
  156. {
  157. message:
  158. 'The path "./require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.',
  159. suggestions: [],
  160. },
  161. ],
  162. },
  163. {
  164. code: 'const filename = "./require-vi-doMock-valid-path2.js"; vi.doMock(filename);',
  165. filename: __filename,
  166. errors: [
  167. {
  168. message:
  169. 'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
  170. suggestions: [],
  171. },
  172. ],
  173. },
  174. ],
  175. })
  176. ruleTester.run(
  177. 'require-cio-snake-case-properties',
  178. requireCioSnakeCaseProperties,
  179. {
  180. valid: [
  181. // updateUserAttributes with snake_case keys
  182. {
  183. code: `CustomerIoHandler.updateUserAttributes(userId, { plan_type: 'free', group_size: 10 })`,
  184. },
  185. // Modules.promises.hooks.fire with snake_case keys
  186. {
  187. code: `Modules.promises.hooks.fire('setUserProperties', userId, { plan_type: 'free', last_active: 123 })`,
  188. },
  189. // Modules.hooks.fire with snake_case keys
  190. {
  191. code: `Modules.hooks.fire('setUserProperties', userId, { plan_type: 'free' })`,
  192. },
  193. // Single-word keys are valid snake_case
  194. {
  195. code: `CustomerIoHandler.updateUserAttributes(userId, { email: 'a@b.com', role: 'admin' })`,
  196. },
  197. // Computed/dynamic keys are skipped
  198. {
  199. code: `CustomerIoHandler.updateUserAttributes(userId, { [dynamicKey]: true })`,
  200. },
  201. // Spread elements are skipped
  202. {
  203. code: `CustomerIoHandler.updateUserAttributes(userId, { ...existingAttrs })`,
  204. },
  205. // Unrelated function calls are not checked
  206. {
  207. code: `SomeOtherHandler.updateUserAttributes(userId, { camelCase: true })`,
  208. },
  209. // fire() with a different event name is not checked
  210. {
  211. code: `Modules.promises.hooks.fire('someOtherEvent', userId, { camelCase: true })`,
  212. },
  213. ],
  214. invalid: [
  215. // camelCase key in updateUserAttributes
  216. {
  217. code: `CustomerIoHandler.updateUserAttributes(userId, { planType: 'free' })`,
  218. errors: [
  219. {
  220. message: `Customer.io attribute 'planType' must be in snake_case.`,
  221. },
  222. ],
  223. },
  224. // kebab-case string key
  225. {
  226. code: `CustomerIoHandler.updateUserAttributes(userId, { 'plan-type': 'free' })`,
  227. errors: [
  228. {
  229. message: `Customer.io attribute 'plan-type' must be in snake_case.`,
  230. },
  231. ],
  232. },
  233. // PascalCase key
  234. {
  235. code: `CustomerIoHandler.updateUserAttributes(userId, { PlanType: 'free' })`,
  236. errors: [
  237. {
  238. message: `Customer.io attribute 'PlanType' must be in snake_case.`,
  239. },
  240. ],
  241. },
  242. // camelCase in Modules.promises.hooks.fire
  243. {
  244. code: `Modules.promises.hooks.fire('setUserProperties', userId, { planType: 'free' })`,
  245. errors: [
  246. {
  247. message: `Customer.io attribute 'planType' must be in snake_case.`,
  248. },
  249. ],
  250. },
  251. // camelCase in Modules.hooks.fire
  252. {
  253. code: `Modules.hooks.fire('setUserProperties', userId, { planType: 'free' })`,
  254. errors: [
  255. {
  256. message: `Customer.io attribute 'planType' must be in snake_case.`,
  257. },
  258. ],
  259. },
  260. // Multiple invalid keys report multiple errors
  261. {
  262. code: `CustomerIoHandler.updateUserAttributes(userId, { planType: 'free', groupSize: 10, plan_term: 'annual' })`,
  263. errors: [
  264. {
  265. message: `Customer.io attribute 'planType' must be in snake_case.`,
  266. },
  267. {
  268. message: `Customer.io attribute 'groupSize' must be in snake_case.`,
  269. },
  270. ],
  271. },
  272. ],
  273. }
  274. )
  275. const noThrowInCallbackMessage =
  276. 'Pass the error to the callback instead of throwing in callback-based code.'
  277. ruleTester.run('no-throw-in-callback', noThrowInCallback, {
  278. valid: [
  279. // Calling the callback with an error is fine
  280. { code: `function foo(cb) { cb(new Error()) }` },
  281. // async functions may throw (they return a rejected promise)
  282. { code: `async function foo(cb) { throw new Error() }` },
  283. // Last param not a callback name — not a callback-style function
  284. { code: `function foo(data) { throw new Error() }` },
  285. // No params at all
  286. { code: `function foo() { throw new Error() }` },
  287. // throw inside a nested non-callback function is fine
  288. { code: `function foo(cb) { [1].map(function() { throw new Error() }) }` },
  289. // throw inside a nested async arrow is fine
  290. { code: `function foo(cb) { [1].map(async () => { throw new Error() }) }` },
  291. ],
  292. invalid: [
  293. {
  294. code: `function foo(cb) { throw new Error() }`,
  295. errors: [{ message: noThrowInCallbackMessage }],
  296. },
  297. {
  298. code: `function foo(callback) { throw new Error() }`,
  299. errors: [{ message: noThrowInCallbackMessage }],
  300. },
  301. {
  302. code: `function foo(done) { throw new Error() }`,
  303. errors: [{ message: noThrowInCallbackMessage }],
  304. },
  305. {
  306. code: `function foo(next) { throw new Error() }`,
  307. errors: [{ message: noThrowInCallbackMessage }],
  308. },
  309. {
  310. code: `function foo(data, cb) { throw new Error() }`,
  311. errors: [{ message: noThrowInCallbackMessage }],
  312. },
  313. {
  314. code: `const foo = (cb) => { throw new Error() }`,
  315. errors: [{ message: noThrowInCallbackMessage }],
  316. },
  317. // throw in a nested callback-style function inside another callback function
  318. {
  319. code: `function foo(cb) { bar(function(done) { throw new Error() }) }`,
  320. errors: [{ message: noThrowInCallbackMessage }],
  321. },
  322. ],
  323. })