@nutin/cli 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +89 -9
  2. package/README.md +20 -35
  3. package/bin/add-feature.mjs +9 -0
  4. package/bin/cli.mjs +8 -0
  5. package/bin/create-app.mjs +5 -4
  6. package/bin/update-app.mjs +9 -0
  7. package/lib/index.mjs +76 -25
  8. package/lib/src/add-feature/feature-adder.mjs +83 -0
  9. package/lib/src/add-feature/feature-context-builder.mjs +25 -0
  10. package/lib/src/add-feature/nutin-config-updater.mjs +29 -0
  11. package/lib/src/add-feature/package-json-updater.mjs +49 -0
  12. package/lib/src/common/feature-registry.mjs +11 -0
  13. package/lib/src/common/file-generator.mjs +159 -0
  14. package/lib/src/common/meta-bootstrap-prompt.mjs +33 -0
  15. package/lib/src/common/package-data.mjs +10 -0
  16. package/lib/src/common/package-json-helper.mjs +103 -0
  17. package/lib/src/common/project-meta.mjs +54 -0
  18. package/lib/src/create-app/context-builder.mjs +21 -0
  19. package/lib/src/{git-manager.mjs → create-app/git-manager.mjs} +2 -2
  20. package/lib/src/create-app/json-manager.mjs +73 -0
  21. package/lib/src/{prompts.mjs → create-app/new-app-prompt.mjs} +6 -6
  22. package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +10 -8
  23. package/lib/src/create-app/success-message.mjs +14 -0
  24. package/lib/src/update-app/conflict-report-writer.mjs +31 -0
  25. package/lib/src/update-app/fetch-old-templates.mjs +61 -0
  26. package/lib/src/update-app/project-updater.mjs +136 -0
  27. package/lib/src/update-app/template-differ.mjs +124 -0
  28. package/lib/src/update-app/update-context-builder.mjs +25 -0
  29. package/lib/src/update-app/update-summary-printer.mjs +22 -0
  30. package/lib/src/update-app/version-compare.mjs +12 -0
  31. package/lib/src/utils/print.mjs +36 -0
  32. package/lib/src/utils/promise-exec-alias.mjs +4 -0
  33. package/package.json +17 -12
  34. package/templates/base/.gitignore.hbs +6 -1
  35. package/templates/base/AGENTS.md.hbs +169 -0
  36. package/templates/base/GETTING_STARTED.md.hbs +227 -0
  37. package/templates/base/README.md.hbs +8 -17
  38. package/templates/base/config/languages.json.hbs +6 -0
  39. package/templates/base/config/seo.json.hbs +12 -0
  40. package/templates/base/nutin.config.js.hbs +51 -0
  41. package/templates/base/src/app/components/index.ts.hbs +1 -2
  42. package/templates/base/src/app/globals.d.ts.hbs +3 -19
  43. package/templates/base/src/{libs/guards → app}/guards.ts.hbs +1 -5
  44. package/templates/base/src/app/main.ts.hbs +7 -26
  45. package/templates/base/src/app/services/index.ts.hbs +1 -1
  46. package/templates/base/src/app/views/home/home.view.html.hbs +7 -0
  47. package/templates/base/src/app/views/home/home.view.ts.hbs +1 -56
  48. package/templates/base/src/app/views/index.ts.hbs +1 -0
  49. package/templates/base/src/app/views/not-found/not-found.view.html.hbs +8 -0
  50. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +4 -37
  51. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +158 -0
  52. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +68 -35
  53. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +124 -0
  54. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +9 -8
  55. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +74 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +1 -1
  57. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +145 -0
  58. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +7 -1
  59. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +88 -0
  60. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +2 -2
  61. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +62 -0
  62. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +82 -0
  63. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +142 -0
  64. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +67 -22
  65. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +183 -0
  66. package/templates/base/src/core/base-classes/component/component.test.js.hbs +85 -0
  67. package/templates/base/src/core/base-classes/component/component.ts.hbs +24 -33
  68. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +58 -0
  69. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +75 -0
  70. package/templates/base/src/core/base-classes/service/service.test.js.hbs +150 -0
  71. package/templates/base/src/core/base-classes/service/service.ts.hbs +26 -20
  72. package/templates/base/src/core/base-classes/view/view.test.js.hbs +74 -0
  73. package/templates/base/src/core/base-classes/view/view.ts.hbs +17 -41
  74. package/templates/base/src/core/config.ts.hbs +7 -0
  75. package/templates/base/src/core/index.ts.hbs +0 -3
  76. package/templates/base/src/core/internals.d.ts.hbs +27 -0
  77. package/templates/base/src/core/nutin-config.d.ts.hbs +4 -0
  78. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +67 -3
  79. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +9 -4
  80. package/templates/base/src/core/services/event-bus/index.ts.hbs +3 -0
  81. package/templates/base/src/core/services/event-bus/lifecycle.facade.test.js.hbs +123 -0
  82. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +61 -0
  83. package/templates/base/src/core/services/event-bus/navigation.facade.test.js.hbs +46 -0
  84. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +25 -0
  85. package/templates/base/src/core/services/globals/globals.test.js.hbs +149 -0
  86. package/templates/base/src/core/services/globals/globals.ts.hbs +95 -0
  87. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +25 -0
  88. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +20 -0
  89. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +43 -0
  90. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  91. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +22 -4
  92. package/templates/base/src/core/services/index.ts.hbs +5 -6
  93. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  94. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  95. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  96. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  97. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +149 -6
  98. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +40 -29
  99. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  100. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  101. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +14 -38
  102. package/templates/base/src/core/services/router/router.test.js.hbs +226 -2
  103. package/templates/base/src/core/services/router/router.ts.hbs +40 -8
  104. package/templates/base/src/index.html.hbs +2 -6
  105. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  106. package/templates/base/src/styles/base/_variables.scss.hbs +1 -86
  107. package/templates/base/src/styles/main.scss.hbs +1 -18
  108. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  109. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  110. package/templates/base/tools/builder/builder.js.hbs +24 -26
  111. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  112. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  113. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  114. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  115. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  116. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  117. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  118. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  119. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  120. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  121. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  122. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  123. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  124. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  125. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  126. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  127. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  128. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  129. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  130. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  131. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  132. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  133. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  134. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  136. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  137. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  138. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  139. package/templates/base/tools/dev/serve.js.hbs +11 -1
  140. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  141. package/templates/base/tools/generator/generator.js.hbs +64 -47
  142. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  143. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  144. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  145. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  146. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  147. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  148. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  149. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +4 -0
  150. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +7 -0
  151. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  152. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  153. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  154. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  155. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  156. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  157. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +3 -2
  158. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  159. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  160. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  164. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  165. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  166. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  167. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  168. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  169. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  170. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  171. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  172. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  173. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  174. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  175. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  176. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  177. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  178. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  179. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  180. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  181. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  182. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  183. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  185. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  186. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  187. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  188. package/templates/base/tools/utils/format.js.hbs +10 -10
  189. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  190. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  191. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  192. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  193. package/templates/base/tools/utils/index.js.hbs +1 -3
  194. package/templates/base/tools/utils/languages.js.hbs +15 -0
  195. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  196. package/templates/base/tools/utils/print.js.hbs +19 -13
  197. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  198. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  199. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  200. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  201. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  202. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  203. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  204. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  205. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  206. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  207. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  208. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  209. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  211. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  212. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  213. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  214. package/lib/src/context-builder.mjs +0 -40
  215. package/lib/src/file-generator.mjs +0 -109
  216. package/lib/src/json-generator.mjs +0 -31
  217. package/lib/src/package-manager.mjs +0 -139
  218. package/lib/src/print.mjs +0 -32
  219. package/lib/src/utils.mjs +0 -18
  220. package/lib/src/version.mjs +0 -1
  221. package/templates/base/builder.config.js.hbs +0 -36
  222. package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +0 -32
  223. package/templates/base/src/app/components/common/button/button.component.ts.hbs +0 -40
  224. package/templates/base/src/app/dom.test.js.hbs +0 -13
  225. package/templates/base/src/app/services/example/example.service.ts.hbs +0 -18
  226. package/templates/base/src/app/views/home/home.view.test.js.hbs +0 -26
  227. package/templates/base/src/core/services/store/store.test.js.hbs +0 -8
  228. package/templates/base/src/core/services/store/store.ts.hbs +0 -39
  229. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.test.js.hbs +0 -0
  230. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +0 -113
  231. package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +0 -249
  232. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +0 -131
  233. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +0 -50
  234. package/templates/base/src/core/utils/index.ts.hbs +0 -2
  235. package/templates/base/src/libs/index.ts.hbs +0 -4
  236. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +0 -197
  237. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +0 -75
  238. package/templates/base/src/libs/popover/popover.ts.hbs +0 -138
  239. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -75
  240. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  241. package/templates/base/src/styles/core/_anchors.scss.hbs +0 -10
  242. package/templates/base/src/styles/core/_index.scss.hbs +0 -6
  243. package/templates/base/src/styles/core/_popover.scss.hbs +0 -96
  244. package/templates/base/src/styles/core/_snackbar.scss.hbs +0 -50
  245. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  246. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  247. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  248. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  249. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  250. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  251. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  252. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  253. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  254. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  255. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  256. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  257. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  258. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  259. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  260. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  261. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  262. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  263. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -254
  264. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  265. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  266. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  267. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  268. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  269. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  270. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  271. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  272. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  273. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  274. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  275. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  276. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  277. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  278. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  279. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  280. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  281. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  282. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  283. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  284. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  285. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  286. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  287. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -0,0 +1,206 @@
1
+ let _installed = false;
2
+ let _now = 0; // virtual clock, ms since epoch
3
+ let _idSeq = 1; // fake timer id counter — starts at 1, never falsy
4
+ const _timers = new Map(); // id -> { id, type: 'timeout'|'interval', callback, args, delay, due }
5
+ let _originals = null; // real fns captured at install time
6
+ const MAX_LOOP_ITERATIONS = 100000;
7
+
8
+ function _schedule(type, cb, delay, args) {
9
+ if (typeof cb !== 'function') {
10
+ throw new Error(`${type === 'timeout' ? 'setTimeout' : 'setInterval'} requires a function callback`);
11
+ }
12
+ const id = _idSeq++;
13
+ const safeDelay = Math.max(0, Number(delay) || 0);
14
+ _timers.set(id, { id, type, callback: cb, args, delay: safeDelay, due: _now + safeDelay });
15
+ return id;
16
+ }
17
+
18
+ const fakeSetTimeout = (cb, delay = 0, ...args) => _schedule('timeout', cb, delay, args);
19
+ const fakeSetInterval = (cb, delay = 0, ...args) => _schedule('interval', cb, delay, args);
20
+ const fakeClearTimeout = (id) => { _timers.delete(id); };
21
+ const fakeClearInterval = fakeClearTimeout;
22
+
23
+ function _makeFakeDate(RealDate) {
24
+ return class FakeDate extends RealDate {
25
+ constructor(...args) {
26
+ if (args.length === 0) {
27
+ super(_now);
28
+ } else {
29
+ super(...args);
30
+ }
31
+ }
32
+ static now() {
33
+ return _now;
34
+ }
35
+ };
36
+ }
37
+
38
+ // Sugar over the fake timers rather than relying on jsdom-setup.js's
39
+ // `window.requestAnimationFrame ??= (cb) => setTimeout(cb, 0)` shim, which is
40
+ // dead code with `pretendToBeVisual: true` jsdom config
41
+ const fakeRequestAnimationFrame = (cb) => fakeSetTimeout(() => cb(_now), 16);
42
+ const fakeCancelAnimationFrame = (id) => fakeClearTimeout(id);
43
+
44
+ function _assertFake(name) {
45
+ if (!_installed) {
46
+ throw new Error(`${name}() requires useFakeTimers() to be called first`);
47
+ }
48
+ }
49
+
50
+ function _earliestDue(predicate) {
51
+ let best = null;
52
+ for (const timer of _timers.values()) {
53
+ if (predicate(timer) && (!best || timer.due < best.due || (timer.due === best.due && timer.id < best.id))) {
54
+ best = timer;
55
+ }
56
+ }
57
+ return best;
58
+ }
59
+
60
+ function _fire(timer) {
61
+ if (timer.type === 'timeout') {
62
+ _timers.delete(timer.id);
63
+ timer.callback(...timer.args);
64
+ } else {
65
+ timer.due += timer.delay;
66
+ timer.callback(...timer.args);
67
+ }
68
+ }
69
+
70
+ global.useFakeTimers = function useFakeTimers() {
71
+ if (_installed) return;
72
+
73
+ _originals = {
74
+ setTimeout: global.setTimeout,
75
+ clearTimeout: global.clearTimeout,
76
+ setInterval: global.setInterval,
77
+ clearInterval: global.clearInterval,
78
+ Date: global.Date,
79
+ requestAnimationFrame: global.requestAnimationFrame,
80
+ cancelAnimationFrame: global.cancelAnimationFrame,
81
+ windowSetTimeout: typeof window !== 'undefined' ? window.setTimeout : undefined,
82
+ windowClearTimeout: typeof window !== 'undefined' ? window.clearTimeout : undefined,
83
+ windowSetInterval: typeof window !== 'undefined' ? window.setInterval : undefined,
84
+ windowClearInterval: typeof window !== 'undefined' ? window.clearInterval : undefined,
85
+ windowDate: typeof window !== 'undefined' ? window.Date : undefined,
86
+ windowRequestAnimationFrame: typeof window !== 'undefined' ? window.requestAnimationFrame : undefined,
87
+ windowCancelAnimationFrame: typeof window !== 'undefined' ? window.cancelAnimationFrame : undefined,
88
+ };
89
+
90
+ _now = _originals.Date.now();
91
+ _idSeq = 1;
92
+ _timers.clear();
93
+
94
+ const FakeDate = _makeFakeDate(_originals.Date);
95
+
96
+ global.setTimeout = fakeSetTimeout;
97
+ global.clearTimeout = fakeClearTimeout;
98
+ global.setInterval = fakeSetInterval;
99
+ global.clearInterval = fakeClearInterval;
100
+ global.Date = FakeDate;
101
+ // Compiled app code calls the bare `requestAnimationFrame` identifier, which
102
+ // resolves via normal scope lookup to `global.requestAnimationFrame` — not
103
+ // `window.requestAnimationFrame` — so both must be faked, not just window's.
104
+ global.requestAnimationFrame = fakeRequestAnimationFrame;
105
+ global.cancelAnimationFrame = fakeCancelAnimationFrame;
106
+
107
+ if (typeof window !== 'undefined') {
108
+ window.setTimeout = fakeSetTimeout;
109
+ window.clearTimeout = fakeClearTimeout;
110
+ window.setInterval = fakeSetInterval;
111
+ window.clearInterval = fakeClearInterval;
112
+ window.Date = FakeDate;
113
+ window.requestAnimationFrame = fakeRequestAnimationFrame;
114
+ window.cancelAnimationFrame = fakeCancelAnimationFrame;
115
+ }
116
+
117
+ _installed = true;
118
+ };
119
+
120
+ global.useRealTimers = function useRealTimers() {
121
+ if (!_installed) return;
122
+
123
+ global.setTimeout = _originals.setTimeout;
124
+ global.clearTimeout = _originals.clearTimeout;
125
+ global.setInterval = _originals.setInterval;
126
+ global.clearInterval = _originals.clearInterval;
127
+ global.Date = _originals.Date;
128
+ global.requestAnimationFrame = _originals.requestAnimationFrame;
129
+ global.cancelAnimationFrame = _originals.cancelAnimationFrame;
130
+
131
+ if (typeof window !== 'undefined') {
132
+ window.setTimeout = _originals.windowSetTimeout;
133
+ window.clearTimeout = _originals.windowClearTimeout;
134
+ window.setInterval = _originals.windowSetInterval;
135
+ window.clearInterval = _originals.windowClearInterval;
136
+ window.Date = _originals.windowDate;
137
+ window.requestAnimationFrame = _originals.windowRequestAnimationFrame;
138
+ window.cancelAnimationFrame = _originals.windowCancelAnimationFrame;
139
+ }
140
+
141
+ _timers.clear();
142
+ _installed = false;
143
+ _originals = null;
144
+ };
145
+
146
+ global.advanceTimersByTime = function advanceTimersByTime(ms) {
147
+ _assertFake('advanceTimersByTime');
148
+ const target = _now + Math.max(0, Number(ms) || 0);
149
+ let iterations = 0;
150
+
151
+ for (;;) {
152
+ const next = _earliestDue(timer => timer.due <= target);
153
+ if (!next) break;
154
+ if (++iterations > MAX_LOOP_ITERATIONS) {
155
+ throw new Error(`advanceTimersByTime: exceeded ${MAX_LOOP_ITERATIONS} timer firings — likely an interval rescheduling itself with delay 0`);
156
+ }
157
+ _now = next.due;
158
+ _fire(next);
159
+ }
160
+
161
+ _now = target;
162
+ };
163
+
164
+ global.tick = global.advanceTimersByTime;
165
+
166
+ global.runAllTimers = function runAllTimers() {
167
+ _assertFake('runAllTimers');
168
+ let iterations = 0;
169
+
170
+ while (_timers.size > 0) {
171
+ if (++iterations > MAX_LOOP_ITERATIONS) {
172
+ throw new Error(`runAllTimers: aborting after ${MAX_LOOP_ITERATIONS} timers — likely an infinite loop (e.g. an uncleared setInterval)`);
173
+ }
174
+ const next = _earliestDue(() => true);
175
+ _now = Math.max(_now, next.due);
176
+ _fire(next);
177
+ }
178
+ };
179
+
180
+ global.runOnlyPendingTimers = function runOnlyPendingTimers() {
181
+ _assertFake('runOnlyPendingTimers');
182
+ const pendingIds = new Set(_timers.keys());
183
+
184
+ for (;;) {
185
+ const next = _earliestDue(timer => pendingIds.has(timer.id));
186
+ if (!next) break;
187
+ pendingIds.delete(next.id);
188
+ _now = Math.max(_now, next.due);
189
+ _fire(next);
190
+ }
191
+ };
192
+
193
+ global.clearAllTimers = function clearAllTimers() {
194
+ _assertFake('clearAllTimers');
195
+ _timers.clear();
196
+ };
197
+
198
+ global.getTimerCount = function getTimerCount() {
199
+ _assertFake('getTimerCount');
200
+ return _timers.size;
201
+ };
202
+
203
+ global.setSystemTime = function setSystemTime(msOrDate) {
204
+ _assertFake('setSystemTime');
205
+ _now = msOrDate instanceof Date ? msOrDate.getTime() : Number(msOrDate);
206
+ };
@@ -1,8 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { JSDOM } from 'jsdom';
4
- import config from '#root/testin-nutin.config.js';
4
+ import { print } from '../../../utils/index.js';
5
+ import config from '#root/nutin.config.js';
6
+
5
7
  let currentDom = null;
8
+ const nativeGlobalKeys = new Set(Object.getOwnPropertyNames(global));
6
9
 
7
10
  export function setupJsdom(htmlPath = 'dist/src/index.html') {
8
11
  const html = fs.readFileSync(path.resolve(process.cwd(), htmlPath), 'utf8');
@@ -12,7 +15,7 @@ export function setupJsdom(htmlPath = 'dist/src/index.html') {
12
15
  resources,
13
16
  freezeGlobals,
14
17
  pretendToBeVisual,
15
- } = config.jsdomOptions;
18
+ } = config.testinNutin.jsdomOptions;
16
19
 
17
20
  const dom = new JSDOM(html, {
18
21
  url: 'http://localhost',
@@ -26,9 +29,11 @@ export function setupJsdom(htmlPath = 'dist/src/index.html') {
26
29
  global.window = dom.window;
27
30
  global.document = dom.window.document;
28
31
 
29
- // DOM globals
32
+ // DOM globals — re-synced every call so classes like HTMLInputElement always
33
+ // match the current jsdom realm, not whichever realm happened to run first.
34
+ // Real Node globals (console, setTimeout, fetch, Event, ...) are left untouched.
30
35
  Object.getOwnPropertyNames(dom.window)
31
- .filter(prop => !(prop in global))
36
+ .filter(prop => !nativeGlobalKeys.has(prop))
32
37
  .forEach(prop => {
33
38
  global[prop] = dom.window[prop];
34
39
  });
@@ -41,7 +46,13 @@ export function setupJsdom(htmlPath = 'dist/src/index.html') {
41
46
  removeEventListener: () => {},
42
47
  });
43
48
 
44
- window.scrollTo ??= () => {};
49
+ // jsdom already ships a real (but "not implemented") scrollTo, so ??= never
50
+ // fires here — always override it with a working no-op.
51
+ window.scrollTo = () => {};
52
+
53
+ // jsdom doesn't implement Element.prototype.scrollIntoView at all (no layout
54
+ // engine to compute a real scroll position), so any code calling it throws.
55
+ window.Element.prototype.scrollIntoView ??= function () {};
45
56
 
46
57
  window.requestAnimationFrame ??= (cb) => setTimeout(cb, 0);
47
58
  window.cancelAnimationFrame ??= (id) => clearTimeout(id);
@@ -72,7 +83,9 @@ export function teardownJsdom() {
72
83
  if (currentDom) {
73
84
  try {
74
85
  currentDom.window.close();
75
- } catch {}
86
+ } catch (err) {
87
+ print.grayError(`[jsdom] window.close() failed: ${err.message}`);
88
+ }
76
89
  }
77
90
 
78
91
  currentDom = null;
@@ -1,8 +1,11 @@
1
- import { print, addTest } from '../../index.js';
2
- import config from '#root/testin-nutin.config.js';
1
+ import { print, addTest } from '../index.js';
3
2
  import { setupJsdom, teardownJsdom, resetDom, flushPromises } from './jsdom-setup.js';
3
+ import { silenceConsole } from './silence-console.js';
4
4
  import './assertion-lib.js';
5
5
  import './spyon.js';
6
+ import './clock.js';
7
+
8
+ const verboseEnabled = process.argv.includes('--verbose');
6
9
 
7
10
  export function registerTestGlobals() {
8
11
  let currentSuite = '';
@@ -16,7 +19,7 @@ export function registerTestGlobals() {
16
19
  afterEach: null,
17
20
  afterAll: null
18
21
  };
19
- if (config.verbose) print.boldSuccess('📂 ' + name);
22
+ if (verboseEnabled) print.boldSuccess('📂 ' + name);
20
23
  fn();
21
24
  };
22
25
 
@@ -31,6 +34,9 @@ export function registerTestGlobals() {
31
34
  afterAll: currentSuiteHooks.afterAll
32
35
  });
33
36
  };
37
+ global.it.todo = (s) => {
38
+ addTest({ suiteName: currentSuite, testName: s, isTodo: true });
39
+ };
34
40
 
35
41
  global.beforeAll = (fn) => {
36
42
  currentSuiteHooks.beforeAll = fn;
@@ -49,11 +55,14 @@ export function registerTestGlobals() {
49
55
  global.teardownJsdom = teardownJsdom;
50
56
  global.resetDom = resetDom;
51
57
  global.flushPromises = flushPromises;
58
+ global.silenceConsole = silenceConsole;
52
59
 
53
60
  global.$ = (selector) => document.querySelector(selector);
54
61
  global.$$ = (selector) => [...document.querySelectorAll(selector)];
55
62
 
56
- global.click = (el) => el.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
63
+ // cancelable: true matches a real browser's native click event, so code
64
+ // under test that calls preventDefault() actually sees defaultPrevented.
65
+ global.click = (el) => el.dispatchEvent(new window.MouseEvent("click", { bubbles: true, cancelable: true }));
57
66
  global.type = (el, text) => { el.value = text; el.dispatchEvent(new window.InputEvent("input", { bubbles: true })); };
58
67
 
59
68
  }
@@ -0,0 +1,23 @@
1
+ export function silenceConsole(methodOrMethods, fn) {
2
+ const methods = Array.isArray(methodOrMethods) ? methodOrMethods : [methodOrMethods];
3
+ const spies = methods.map((method) => spyOn(console, method).andCallFake(() => {}));
4
+ const restore = () => spies.forEach((spy) => spy.restore());
5
+
6
+ let result;
7
+ try {
8
+ result = fn();
9
+ } catch (err) {
10
+ restore();
11
+ throw err;
12
+ }
13
+
14
+ if (result && typeof result.then === 'function') {
15
+ return result.then(
16
+ (value) => { restore(); return value; },
17
+ (err) => { restore(); throw err; }
18
+ );
19
+ }
20
+
21
+ restore();
22
+ return result;
23
+ }
@@ -47,11 +47,13 @@ global.spyOn = (obj, methodName) => {
47
47
  andCallFake(fn) {
48
48
  spyFunction._fake = fn;
49
49
  spyFunction._hasReturn = false;
50
+ return handle;
50
51
  },
51
52
  andReturn(value) {
52
53
  spyFunction._returnValue = value;
53
54
  spyFunction._hasReturn = true;
54
55
  spyFunction._fake = null;
56
+ return handle;
55
57
  }
56
58
  };
57
59
 
@@ -0,0 +1,9 @@
1
+ export * from './globals/jsdom-setup.js';
2
+ export * from './globals/register-test-globals.js';
3
+ export * from './queue/test-discovery.js';
4
+ export * from './queue/test-queue.js';
5
+ export * from './printer.js';
6
+ export * from './coverage/collect-coverage.js';
7
+ export * from './coverage/compute-coverage.js';
8
+ export * from './coverage/write-uncovered-report.js';
9
+ export * from './coverage/write-summary-report.js';
@@ -0,0 +1,85 @@
1
+ import { print, chalk } from '../../utils/index.js';
2
+ import config from '#root/nutin.config.js';
3
+
4
+ export { chalk, print } from '../../utils/index.js';
5
+
6
+ const verboseEnabled = process.argv.includes('--verbose');
7
+
8
+ export function printSummary(todo, passed, failed, time) {
9
+ const totalTime = time.toFixed(2);
10
+ const total = passed + failed;
11
+
12
+ print.boldBlue(`Test Summary`);
13
+ print.boldInfo(`✓ ${passed} passed`);
14
+ print.boldError(`✗ ${failed} failed`);
15
+ print.info(`📖 ${total} total`);
16
+ print.info(`⌚️ Time: ${totalTime}ms\n`);
17
+ if (todo > 0) print.boldWarn(`⚠️ To do: ${todo}\n`);
18
+ }
19
+
20
+ export function printResults(results) {
21
+ const statusOrder = { passed: 0, todo: 1, failed: 2 };
22
+ const sortedResults = results.sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
23
+
24
+ sortedResults.forEach(result => {
25
+ if (result.status === 'passed') {
26
+ if (verboseEnabled) console.log(`${chalk.green('✓')} ${chalk.cyan(`${result.suiteName}`)} - ${chalk.green(`${result.name}`)}`);
27
+ } else if (result.status === 'todo') {
28
+ print.warn(`TODO: ${result.name} in ${toTsPath(result.file)}`);
29
+ } else {
30
+ print.error(`✗ ${result.suiteName} - ${result.name}`);
31
+ print.grayError(result.error);
32
+ }
33
+ });
34
+ }
35
+
36
+ export function printStart() {
37
+ print.boldHead('\nRunning tests...\n');
38
+ }
39
+
40
+ export function printCoverageReport(report) {
41
+ if (!report.length) {
42
+ print.warn('No app source files were covered.');
43
+ return { globalLinesPct: 100, globalFunctionsPct: 100, globalBranchesPct: 100 };
44
+ }
45
+
46
+ print.boldBlue('\nCoverage Summary');
47
+ console.table(
48
+ report
49
+ .sort((a, b) => a.file.localeCompare(b.file))
50
+ .map(r => ({
51
+ File: toTsPath(r.file),
52
+ 'Lines %': r.linesPct.toFixed(1),
53
+ 'Functions %': r.functionsPct.toFixed(1),
54
+ 'Branches %': r.branchesPct.toFixed(1),
55
+ }))
56
+ );
57
+
58
+ const totals = report.reduce((acc, r) => ({
59
+ coveredLines: acc.coveredLines + r.coveredLines,
60
+ totalLines: acc.totalLines + r.totalLines,
61
+ coveredBranches: acc.coveredBranches + r.coveredBranches,
62
+ totalBranches: acc.totalBranches + r.totalBranches,
63
+ coveredFunctions: acc.coveredFunctions + r.coveredFunctions,
64
+ totalFunctions: acc.totalFunctions + r.totalFunctions,
65
+ }), { coveredLines: 0, totalLines: 0, coveredBranches: 0, totalBranches: 0, coveredFunctions: 0, totalFunctions: 0 });
66
+
67
+ const globalLinesPct = totals.totalLines ? (totals.coveredLines / totals.totalLines) * 100 : 100;
68
+ const globalFunctionsPct = totals.totalFunctions ? (totals.coveredFunctions / totals.totalFunctions) * 100 : 100;
69
+ const globalBranchesPct = totals.totalBranches ? (totals.coveredBranches / totals.totalBranches) * 100 : 100;
70
+
71
+ const threshold = config.testinNutin.coverage?.threshold;
72
+ const thresholdSuffix = typeof threshold === 'number' ? ` (threshold: ${threshold}%)` : '';
73
+ print.boldInfo(`Global: ${globalBranchesPct.toFixed(1)}% branches, ${globalFunctionsPct.toFixed(1)}% functions, ${globalLinesPct.toFixed(1)}% lines${thresholdSuffix}\n`);
74
+
75
+ return { globalLinesPct, globalFunctionsPct, globalBranchesPct };
76
+ }
77
+
78
+ export function toTsPath(input) {
79
+ return input
80
+ .split('/')
81
+ .slice(1)
82
+ .join('/')
83
+ .replace(/\.js$/, '.ts');
84
+ }
85
+
@@ -0,0 +1,46 @@
1
+ import Queue from './queue.js';
2
+
3
+ describe('Queue', () => {
4
+ it('starts empty', () => {
5
+ const queue = new Queue();
6
+ expect(queue.length).toBe(0);
7
+ expect(queue.deque()).toBe(undefined);
8
+ expect(queue.peek()).toBe(undefined);
9
+ });
10
+
11
+ it('dequeues items in FIFO order', () => {
12
+ const queue = new Queue();
13
+ queue.enqueue('a');
14
+ queue.enqueue('b');
15
+ queue.enqueue('c');
16
+
17
+ expect(queue.length).toBe(3);
18
+ expect(queue.deque()).toBe('a');
19
+ expect(queue.deque()).toBe('b');
20
+ expect(queue.length).toBe(1);
21
+ expect(queue.deque()).toBe('c');
22
+ expect(queue.length).toBe(0);
23
+ });
24
+
25
+ it('peek returns the next item without removing it', () => {
26
+ const queue = new Queue();
27
+ queue.enqueue('first');
28
+ queue.enqueue('second');
29
+
30
+ expect(queue.peek()).toBe('first');
31
+ expect(queue.peek()).toBe('first');
32
+ expect(queue.length).toBe(2);
33
+ });
34
+
35
+ it('handles a single enqueue/deque cycle and can be reused afterward', () => {
36
+ const queue = new Queue();
37
+ queue.enqueue('only');
38
+ expect(queue.deque()).toBe('only');
39
+ expect(queue.length).toBe(0);
40
+ expect(queue.deque()).toBe(undefined);
41
+
42
+ queue.enqueue('again');
43
+ expect(queue.length).toBe(1);
44
+ expect(queue.deque()).toBe('again');
45
+ });
46
+ });
@@ -0,0 +1,46 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { getTestFiles } from './test-discovery.js';
6
+
7
+ // getTestFiles resolves its own `projectRoot` anchor one directory short of the real
8
+ // project root (see tools/testin-nutin/runner.js, which always calls it as
9
+ // `getTestFiles(path.join('..', origin))` to compensate). This test file lives in the
10
+ // same directory as test-discovery.js, so it can recompute that same anchor to build
11
+ // `dir` arguments the same way the real runner does.
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const relativeToCwd = path.relative(process.cwd(), __dirname);
14
+ const levelsUp = relativeToCwd.split(path.sep).length - 1;
15
+ const projectRoot = path.resolve(__dirname, ...Array(levelsUp).fill('..'));
16
+
17
+ describe('getTestFiles', () => {
18
+ let tmpDir;
19
+ let relDir;
20
+
21
+ beforeEach(() => {
22
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-test-discovery-'));
23
+ relDir = path.relative(projectRoot, tmpDir);
24
+
25
+ fs.mkdirSync(path.join(tmpDir, 'nested'), { recursive: true });
26
+ fs.writeFileSync(path.join(tmpDir, 'a.test.js'), '');
27
+ fs.writeFileSync(path.join(tmpDir, 'b.js'), '');
28
+ fs.writeFileSync(path.join(tmpDir, 'nested', 'c.test.js'), '');
29
+ fs.writeFileSync(path.join(tmpDir, 'nested', 'readme.md'), '');
30
+ });
31
+
32
+ afterEach(() => {
33
+ fs.rmSync(tmpDir, { recursive: true, force: true });
34
+ });
35
+
36
+ it('recursively finds only files ending in .test.js', () => {
37
+ const files = getTestFiles(relDir).sort();
38
+ expect(files).toEqual(
39
+ [path.join(tmpDir, 'a.test.js'), path.join(tmpDir, 'nested', 'c.test.js')].sort()
40
+ );
41
+ });
42
+
43
+ it('returns an empty array for a directory that does not exist', () => {
44
+ expect(getTestFiles(path.join(relDir, 'does-not-exist'))).toEqual([]);
45
+ });
46
+ });