@nutin/cli 1.3.0 → 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 (284) hide show
  1. package/CHANGELOG.md +122 -8
  2. package/README.md +21 -37
  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 +81 -30
  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/create-app/new-app-prompt.mjs +51 -0
  22. package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +15 -13
  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 -15
  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 +81 -31
  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 +38 -23
  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 +41 -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 +13 -14
  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 +27 -40
  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/config.helper.ts.hbs +17 -0
  70. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +75 -0
  71. package/templates/base/src/core/base-classes/service/service.test.js.hbs +150 -0
  72. package/templates/base/src/core/base-classes/service/service.ts.hbs +26 -20
  73. package/templates/base/src/core/base-classes/view/view.test.js.hbs +74 -0
  74. package/templates/base/src/core/base-classes/view/view.ts.hbs +17 -41
  75. package/templates/base/src/core/config.ts.hbs +7 -0
  76. package/templates/base/src/core/index.ts.hbs +0 -3
  77. package/templates/base/src/core/internals.d.ts.hbs +27 -0
  78. package/templates/base/src/core/nutin-config.d.ts.hbs +4 -0
  79. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +67 -3
  80. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +9 -4
  81. package/templates/base/src/core/services/event-bus/index.ts.hbs +3 -0
  82. package/templates/base/src/core/services/event-bus/lifecycle.facade.test.js.hbs +123 -0
  83. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +61 -0
  84. package/templates/base/src/core/services/event-bus/navigation.facade.test.js.hbs +46 -0
  85. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +25 -0
  86. package/templates/base/src/core/services/globals/globals.test.js.hbs +149 -0
  87. package/templates/base/src/core/services/globals/globals.ts.hbs +95 -0
  88. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +25 -0
  89. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +20 -0
  90. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +43 -0
  91. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  92. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +60 -9
  93. package/templates/base/src/core/services/index.ts.hbs +5 -6
  94. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  95. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  96. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  97. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  98. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +149 -6
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +42 -30
  100. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  101. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  102. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +13 -70
  103. package/templates/base/src/core/services/router/router.test.js.hbs +226 -2
  104. package/templates/base/src/core/services/router/router.ts.hbs +40 -9
  105. package/templates/base/src/index.html.hbs +2 -6
  106. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  107. package/templates/base/src/styles/base/_variables.scss.hbs +1 -85
  108. package/templates/base/src/styles/main.scss.hbs +1 -18
  109. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  110. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  111. package/templates/base/tools/builder/builder.js.hbs +24 -26
  112. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  113. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  114. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  115. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  116. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  117. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  118. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  119. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  120. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  121. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  122. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  123. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  124. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  125. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  126. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  127. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  128. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  129. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  130. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  131. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  132. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  133. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  134. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  136. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  137. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  138. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  139. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  140. package/templates/base/tools/dev/serve.js.hbs +11 -1
  141. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  142. package/templates/base/tools/generator/generator.js.hbs +64 -47
  143. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  144. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  145. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  146. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  147. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  148. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  149. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  150. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +4 -0
  151. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +7 -0
  152. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  153. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  154. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  155. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  156. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  157. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  158. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +3 -2
  159. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  160. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  164. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  165. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  166. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  167. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  168. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  169. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  170. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  171. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  172. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  173. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  174. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  175. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  176. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  177. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  178. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  179. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  180. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  181. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  182. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  183. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  185. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  186. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  187. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  188. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  189. package/templates/base/tools/utils/format.js.hbs +10 -10
  190. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  191. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  192. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  193. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  194. package/templates/base/tools/utils/index.js.hbs +1 -3
  195. package/templates/base/tools/utils/languages.js.hbs +15 -0
  196. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  197. package/templates/base/tools/utils/print.js.hbs +19 -13
  198. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  199. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  200. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  201. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  202. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  203. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  204. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  205. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  206. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  207. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  208. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  209. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  211. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  212. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  213. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  214. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  215. package/lib/src/context-builder.mjs +0 -35
  216. package/lib/src/file-generator.mjs +0 -109
  217. package/lib/src/json-generator.mjs +0 -31
  218. package/lib/src/package-manager.mjs +0 -139
  219. package/lib/src/print.mjs +0 -31
  220. package/lib/src/prompts.mjs +0 -123
  221. package/lib/src/utils.mjs +0 -18
  222. package/lib/src/version.mjs +0 -1
  223. package/templates/base/builder.config.js.hbs +0 -35
  224. package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +0 -32
  225. package/templates/base/src/app/components/common/button/button.component.ts.hbs +0 -40
  226. package/templates/base/src/app/dom.test.js.hbs +0 -13
  227. package/templates/base/src/app/services/example/example.service.ts.hbs +0 -18
  228. package/templates/base/src/app/views/home/home.view.test.js.hbs +0 -26
  229. package/templates/base/src/core/services/store/store.test.js.hbs +0 -8
  230. package/templates/base/src/core/services/store/store.ts.hbs +0 -39
  231. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.test.js.hbs +0 -0
  232. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +0 -85
  233. package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +0 -249
  234. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +0 -108
  235. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +0 -50
  236. package/templates/base/src/core/utils/index.ts.hbs +0 -2
  237. package/templates/base/src/libs/index.ts.hbs +0 -4
  238. package/templates/base/src/libs/popover/popover.ts.hbs +0 -145
  239. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -72
  240. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  241. package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
  242. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  243. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  244. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  245. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  246. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  247. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  248. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  249. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  250. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  251. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  252. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  253. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  254. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  255. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  256. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  257. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  258. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  259. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  260. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -240
  261. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  262. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  263. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  264. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  265. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  266. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  267. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  268. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  269. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  270. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  271. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  272. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  273. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  274. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  275. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  276. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  277. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  278. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  279. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  280. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  281. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  282. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  283. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  284. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -0,0 +1,123 @@
1
+ import { Lifecycle, AppEventBus } from '#root/dist/src/core/index.js';
2
+
3
+ describe('LifecycleFacade (Lifecycle)', () => {
4
+ afterEach(() => {
5
+ AppEventBus.cleanupEventListeners();
6
+ });
7
+
8
+ it('beforeRender emits "before-render"', () => {
9
+ const emit = spyOn(AppEventBus, 'emit');
10
+ Lifecycle.beforeRender();
11
+ expect(emit).toHaveBeenCalledWith('before-render');
12
+ emit.restore();
13
+ });
14
+
15
+ it('onBeforeRender subscribes to "before-render", and the returned unsubscribe removes it', () => {
16
+ let called = 0;
17
+ const unsubscribe = Lifecycle.onBeforeRender(() => called++);
18
+
19
+ Lifecycle.beforeRender();
20
+ expect(called).toBe(1);
21
+
22
+ unsubscribe();
23
+ Lifecycle.beforeRender();
24
+ expect(called).toBe(1);
25
+ });
26
+
27
+ it('afterRender emits "after-render"', () => {
28
+ const emit = spyOn(AppEventBus, 'emit');
29
+ Lifecycle.afterRender();
30
+ expect(emit).toHaveBeenCalledWith('after-render');
31
+ emit.restore();
32
+ });
33
+
34
+ it('onAfterRender subscribes to "after-render", and the returned unsubscribe removes it', () => {
35
+ let called = 0;
36
+ const unsubscribe = Lifecycle.onAfterRender(() => called++);
37
+
38
+ Lifecycle.afterRender();
39
+ expect(called).toBe(1);
40
+
41
+ unsubscribe();
42
+ Lifecycle.afterRender();
43
+ expect(called).toBe(1);
44
+ });
45
+
46
+ it('beforeDestroy emits "before-destroy"', () => {
47
+ const emit = spyOn(AppEventBus, 'emit');
48
+ Lifecycle.beforeDestroy();
49
+ expect(emit).toHaveBeenCalledWith('before-destroy');
50
+ emit.restore();
51
+ });
52
+
53
+ it('onBeforeDestroy subscribes to "before-destroy", and the returned unsubscribe removes it', () => {
54
+ let called = 0;
55
+ const unsubscribe = Lifecycle.onBeforeDestroy(() => called++);
56
+
57
+ Lifecycle.beforeDestroy();
58
+ expect(called).toBe(1);
59
+
60
+ unsubscribe();
61
+ Lifecycle.beforeDestroy();
62
+ expect(called).toBe(1);
63
+ });
64
+
65
+ it('afterDestroy emits "after-destroy"', () => {
66
+ const emit = spyOn(AppEventBus, 'emit');
67
+ Lifecycle.afterDestroy();
68
+ expect(emit).toHaveBeenCalledWith('after-destroy');
69
+ emit.restore();
70
+ });
71
+
72
+ it('onAfterDestroy subscribes to "after-destroy", and the returned unsubscribe removes it', () => {
73
+ let called = 0;
74
+ const unsubscribe = Lifecycle.onAfterDestroy(() => called++);
75
+
76
+ Lifecycle.afterDestroy();
77
+ expect(called).toBe(1);
78
+
79
+ unsubscribe();
80
+ Lifecycle.afterDestroy();
81
+ expect(called).toBe(1);
82
+ });
83
+
84
+ it('viewMount emits "view-mount" with { viewName }', () => {
85
+ const emit = spyOn(AppEventBus, 'emit');
86
+ Lifecycle.viewMount('home');
87
+ expect(emit).toHaveBeenCalledWith('view-mount', { viewName: 'home' });
88
+ emit.restore();
89
+ });
90
+
91
+ it('onViewMount subscribes to "view-mount", and the returned unsubscribe removes it', () => {
92
+ let received = null;
93
+ const unsubscribe = Lifecycle.onViewMount((data) => { received = data; });
94
+
95
+ Lifecycle.viewMount('home');
96
+ expect(received).toEqual({ viewName: 'home' });
97
+
98
+ unsubscribe();
99
+ received = null;
100
+ Lifecycle.viewMount('other');
101
+ expect(received).toBe(null);
102
+ });
103
+
104
+ it('viewUnmount emits "view-unmount" with { viewName }', () => {
105
+ const emit = spyOn(AppEventBus, 'emit');
106
+ Lifecycle.viewUnmount('home');
107
+ expect(emit).toHaveBeenCalledWith('view-unmount', { viewName: 'home' });
108
+ emit.restore();
109
+ });
110
+
111
+ it('onViewUnmount subscribes to "view-unmount", and the returned unsubscribe removes it', () => {
112
+ let received = null;
113
+ const unsubscribe = Lifecycle.onViewUnmount((data) => { received = data; });
114
+
115
+ Lifecycle.viewUnmount('home');
116
+ expect(received).toEqual({ viewName: 'home' });
117
+
118
+ unsubscribe();
119
+ received = null;
120
+ Lifecycle.viewUnmount('other');
121
+ expect(received).toBe(null);
122
+ });
123
+ });
@@ -0,0 +1,61 @@
1
+ import { AppEventBus, IEventBus } from "./event-bus.js";
2
+
3
+ class LifecycleFacade {
4
+ constructor(private bus: IEventBus) {}
5
+
6
+ beforeRender(): void {
7
+ this.bus.emit('before-render');
8
+ }
9
+
10
+ onBeforeRender(callback: () => void): () => void {
11
+ this.bus.subscribe('before-render', callback);
12
+ return () => this.bus.off('before-render', callback);
13
+ }
14
+
15
+ afterRender(): void {
16
+ this.bus.emit('after-render');
17
+ }
18
+
19
+ onAfterRender(callback: () => void): () => void {
20
+ this.bus.subscribe('after-render', callback);
21
+ return () => this.bus.off('after-render', callback);
22
+ }
23
+
24
+ beforeDestroy(): void {
25
+ this.bus.emit('before-destroy');
26
+ }
27
+
28
+ onBeforeDestroy(callback: () => void): () => void {
29
+ this.bus.subscribe('before-destroy', callback);
30
+ return () => this.bus.off('before-destroy', callback);
31
+ }
32
+
33
+ afterDestroy(): void {
34
+ this.bus.emit('after-destroy');
35
+ }
36
+
37
+ onAfterDestroy(callback: () => void): () => void {
38
+ this.bus.subscribe('after-destroy', callback);
39
+ return () => this.bus.off('after-destroy', callback);
40
+ }
41
+
42
+ viewMount(viewName: string): void {
43
+ this.bus.emit('view-mount', {viewName});
44
+ }
45
+
46
+ onViewMount(callback: (data: { viewName: string }) => void): () => void {
47
+ this.bus.subscribe('view-mount', callback);
48
+ return () => this.bus.off('view-mount', callback);
49
+ }
50
+
51
+ viewUnmount(viewName: string): void {
52
+ this.bus.emit('view-unmount', { viewName });
53
+ }
54
+
55
+ onViewUnmount(callback: (data: { viewName: string }) => void): () => void {
56
+ this.bus.subscribe('view-unmount', callback);
57
+ return () => this.bus.off('view-unmount', callback);
58
+ }
59
+ }
60
+
61
+ export const Lifecycle = new LifecycleFacade(AppEventBus);
@@ -0,0 +1,46 @@
1
+ import { Navigation, AppEventBus } from '#root/dist/src/core/index.js';
2
+
3
+ describe('NavigationFacade (Navigation)', () => {
4
+ afterEach(() => {
5
+ AppEventBus.cleanupEventListeners();
6
+ });
7
+
8
+ it('navigateTo emits "navigate" with { path }', () => {
9
+ const emit = spyOn(AppEventBus, 'emit');
10
+ Navigation.navigateTo('/about');
11
+ expect(emit).toHaveBeenCalledWith('navigate', { path: '/about' });
12
+ emit.restore();
13
+ });
14
+
15
+ it('onNavigate subscribes to "navigate", and the returned unsubscribe removes it', () => {
16
+ let received = null;
17
+ const unsubscribe = Navigation.onNavigate((data) => { received = data; });
18
+
19
+ Navigation.navigateTo('/about');
20
+ expect(received).toEqual({ path: '/about' });
21
+
22
+ unsubscribe();
23
+ received = null;
24
+ Navigation.navigateTo('/contact');
25
+ expect(received).toBe(null);
26
+ });
27
+
28
+ it('reload emits "reload"', () => {
29
+ const emit = spyOn(AppEventBus, 'emit');
30
+ Navigation.reload();
31
+ expect(emit).toHaveBeenCalledWith('reload');
32
+ emit.restore();
33
+ });
34
+
35
+ it('onReload subscribes to "reload", and the returned unsubscribe removes it', () => {
36
+ let called = 0;
37
+ const unsubscribe = Navigation.onReload(() => called++);
38
+
39
+ Navigation.reload();
40
+ expect(called).toBe(1);
41
+
42
+ unsubscribe();
43
+ Navigation.reload();
44
+ expect(called).toBe(1);
45
+ });
46
+ });
@@ -0,0 +1,25 @@
1
+ import { AppEventBus, IEventBus } from "./event-bus.js";
2
+
3
+ class NavigationFacade {
4
+ constructor(private bus: IEventBus) {}
5
+
6
+ navigateTo(path: string): void {
7
+ this.bus.emit('navigate', { path });
8
+ }
9
+
10
+ onNavigate(callback: (data: { path: string }) => void): () => void {
11
+ this.bus.subscribe('navigate', callback);
12
+ return () => this.bus.off('navigate', callback);
13
+ }
14
+
15
+ reload(): void {
16
+ this.bus.emit('reload');
17
+ }
18
+
19
+ onReload(callback: () => void): () => void {
20
+ this.bus.subscribe('reload', callback);
21
+ return () => this.bus.off('reload', callback);
22
+ }
23
+ }
24
+
25
+ export const Navigation = new NavigationFacade(AppEventBus);
@@ -0,0 +1,149 @@
1
+ import { registerGlobals, hideGlobals, revealGlobals, Globals } from '#root/dist/src/core/services/globals/globals.js';
2
+
3
+ function makeComponent(tag = 'div') {
4
+ return class {
5
+ constructor(mountTarget) {
6
+ this.mountTarget = mountTarget;
7
+ }
8
+ render() {
9
+ return document.createElement(tag);
10
+ }
11
+ };
12
+ }
13
+
14
+ describe('globals (registerGlobals)', () => {
15
+ afterEach(() => {
16
+ // registerGlobals mounts directly onto document.body (a sibling of the
17
+ // pre-existing <main id="app">), so only strip what each test added.
18
+ [...document.body.children].forEach((el) => {
19
+ if (el.id !== 'app') el.remove();
20
+ });
21
+ // Globals is a process-wide singleton (like every other Service) - drop
22
+ // its registered-id state so a later test's ids can't collide with an
23
+ // earlier one's.
24
+ Globals.testingResetAll();
25
+ });
26
+
27
+ it('is defined', () => {
28
+ expect(registerGlobals).toBeDefined();
29
+ });
30
+
31
+ it('touches nothing when called with no options', () => {
32
+ registerGlobals({});
33
+ expect(document.body.children.length).toBe(1); // just #app
34
+ });
35
+
36
+ it('prepends a single "before" entry ahead of the existing #app, stamping its id', () => {
37
+ registerGlobals({ before: [{ component: makeComponent('header'), id: 'header' }] });
38
+ expect(document.body.firstElementChild.tagName).toBe('HEADER');
39
+ expect(document.body.firstElementChild.id).toBe('header');
40
+ expect(document.body.children[1].id).toBe('app');
41
+ });
42
+
43
+ it('appends a single "after" entry behind the existing #app, stamping its id', () => {
44
+ registerGlobals({ after: [{ component: makeComponent('footer'), id: 'footer' }] });
45
+ expect(document.body.lastElementChild.tagName).toBe('FOOTER');
46
+ expect(document.body.lastElementChild.id).toBe('footer');
47
+ });
48
+
49
+ it('mounts multiple "before" entries in the given order (not reversed)', () => {
50
+ registerGlobals({
51
+ before: [
52
+ { component: makeComponent('div'), id: 'first' },
53
+ { component: makeComponent('div'), id: 'second' },
54
+ ],
55
+ });
56
+ const ids = [...document.body.children].filter((el) => el.id !== 'app').map((el) => el.id);
57
+ expect(ids).toEqual(['first', 'second']);
58
+ });
59
+
60
+ it('mounts multiple "after" entries in the given order (not reversed)', () => {
61
+ registerGlobals({
62
+ after: [
63
+ { component: makeComponent('div'), id: 'first' },
64
+ { component: makeComponent('div'), id: 'second' },
65
+ ],
66
+ });
67
+ const ids = [...document.body.children].filter((el) => el.id !== 'app').map((el) => el.id);
68
+ expect(ids).toEqual(['first', 'second']);
69
+ });
70
+
71
+ it('passes the same mountTarget value through to every constructed component', () => {
72
+ const seen = [];
73
+ class TrackingComponent {
74
+ constructor(mountTarget) {
75
+ seen.push(mountTarget);
76
+ }
77
+ render() {
78
+ return document.createElement('div');
79
+ }
80
+ }
81
+ registerGlobals({
82
+ before: [{ component: TrackingComponent, id: 'a' }],
83
+ after: [{ component: TrackingComponent, id: 'b' }],
84
+ });
85
+ expect(seen).toEqual(['body', 'body']);
86
+ });
87
+
88
+ it('registering a duplicate id warns and skips the second mount', () => {
89
+ const warnSpy = spyOn(console, 'warn');
90
+ warnSpy.andCallFake(() => {});
91
+
92
+ registerGlobals({ before: [{ component: makeComponent('header'), id: 'dup' }] });
93
+ registerGlobals({ after: [{ component: makeComponent('footer'), id: 'dup' }] });
94
+
95
+ const matches = [...document.body.children].filter((el) => el.id === 'dup');
96
+ expect(matches.length).toBe(1);
97
+ expect(matches[0].tagName).toBe('HEADER');
98
+ expect(warnSpy.callCount).toBe(1);
99
+
100
+ warnSpy.restore();
101
+ });
102
+ });
103
+
104
+ describe('globals (hideGlobals/revealGlobals)', () => {
105
+ afterEach(() => {
106
+ [...document.body.children].forEach((el) => {
107
+ if (el.id !== 'app') el.remove();
108
+ });
109
+ });
110
+
111
+ it('toggle display for every given id that is present', () => {
112
+ const header = document.createElement('div');
113
+ header.id = 'header';
114
+ document.body.appendChild(header);
115
+ const footer = document.createElement('div');
116
+ footer.id = 'footer';
117
+ document.body.appendChild(footer);
118
+
119
+ hideGlobals(['header', 'footer']);
120
+ expect(header.style.display).toBe('none');
121
+ expect(footer.style.display).toBe('none');
122
+
123
+ revealGlobals(['header', 'footer']);
124
+ expect(header.style.display).toBe('block');
125
+ expect(footer.style.display).toBe('block');
126
+ });
127
+
128
+ it('only touch the display property, leaving other inline styles intact', () => {
129
+ const header = document.createElement('div');
130
+ header.id = 'header';
131
+ header.style.cssText = 'background: coral; padding: 8px';
132
+ document.body.appendChild(header);
133
+
134
+ hideGlobals(['header']);
135
+ expect(header.style.display).toBe('none');
136
+ expect(header.style.background).toBe('coral');
137
+ expect(header.style.padding).toBe('8px');
138
+
139
+ revealGlobals(['header']);
140
+ expect(header.style.display).toBe('block');
141
+ expect(header.style.background).toBe('coral');
142
+ expect(header.style.padding).toBe('8px');
143
+ });
144
+
145
+ it('are no-ops for ids that are absent', () => {
146
+ expect(() => hideGlobals(['missing'])).not.toThrow();
147
+ expect(() => revealGlobals(['missing'])).not.toThrow();
148
+ });
149
+ });
@@ -0,0 +1,95 @@
1
+ import { Service } from '../../base-classes/index.js';
2
+
3
+ export interface GlobalMountable {
4
+ render(): HTMLElement;
5
+ destroy?(): void;
6
+ }
7
+
8
+ export interface GlobalConfig<T extends GlobalMountable = GlobalMountable> {
9
+ /** The component's own class — constructed internally as `new component(mountTarget)`. */
10
+ component: new (mountTarget: HTMLElement) => T;
11
+ /** Stamped onto the mounted root element; pass this same id to `hideGlobals`/`revealGlobals`. */
12
+ id: string;
13
+ }
14
+
15
+ export interface RegisterGlobalsOptions {
16
+ /** Rendered and prepended to `<body>`, in the given order (e.g. a header). */
17
+ before?: GlobalConfig[];
18
+ /** Rendered and appended to `<body>`, in the given order (e.g. a footer). */
19
+ after?: GlobalConfig[];
20
+ }
21
+
22
+ export class Globals extends Service<Globals> {
23
+ private _mounted: Record<string, GlobalMountable> = {};
24
+
25
+ constructor() { super(); }
26
+
27
+ public register({ before = [], after = [] }: RegisterGlobalsOptions): void {
28
+ // Component subclasses generated via `nutin generate component` type their
29
+ // mountTarget param as HTMLElement, but DomHelper.mountElement only
30
+ // *appends* (rather than replacing) the target when mountTarget is a string
31
+ // selector. This cast satisfies the stricter constructor type while still
32
+ // passing the string 'body' through at runtime, so components append into
33
+ // <body> instead of replacing it.
34
+ const mountTarget = 'body' as unknown as HTMLElement;
35
+ const isElement = (el: HTMLElement | null): el is HTMLElement => el !== null;
36
+
37
+ const mount = ({ component: Component, id }: GlobalConfig): HTMLElement | null => {
38
+ if (this._mounted[id]) {
39
+ console.warn(`Global "${id}" is already registered - skipping.`);
40
+ return null;
41
+ }
42
+ const instance = new Component(mountTarget);
43
+ const element = instance.render();
44
+ element.id = id;
45
+ this._mounted[id] = instance;
46
+ return element;
47
+ };
48
+
49
+ document.body.prepend(...before.map(mount).filter(isElement));
50
+ document.body.append(...after.map(mount).filter(isElement));
51
+ }
52
+
53
+ public hide(globalIds: string[]): void {
54
+ this.setDisplay(globalIds, 'none');
55
+ }
56
+
57
+ public reveal(globalIds: string[]): void {
58
+ this.setDisplay(globalIds, 'block');
59
+ }
60
+
61
+ private setDisplay(globalIds: string[], display: 'none' | 'block'): void {
62
+ globalIds.forEach((id) => {
63
+ const el = document.getElementById(id) as HTMLElement | null;
64
+ if (el) el.style.display = display;
65
+ });
66
+ }
67
+
68
+ protected onDestroy(): void {
69
+ Object.values(this._mounted).forEach((instance) => instance.destroy?.());
70
+ this._mounted = {};
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Mounts components outside the router's render target (`<main id="app">`),
76
+ * e.g. a header/footer that should persist across every route. Each entry's
77
+ * `component` is constructed internally — callers never see or handle
78
+ * `mountTarget` themselves — and its rendered root element is given the
79
+ * entry's `id`, so `hideGlobals(ids)`/`revealGlobals(ids)` can target it
80
+ * later by that same id. Registering an `id` that's already registered logs
81
+ * a `console.warn` and skips it.
82
+ */
83
+ export const registerGlobals = (options: RegisterGlobalsOptions): void => {
84
+ Globals.getInstance().register(options);
85
+ };
86
+
87
+ /** Hide the elements with the given ids (as registered via `registerGlobals`). */
88
+ export const hideGlobals = (globalIds: string[]): void => {
89
+ Globals.getInstance().hide(globalIds);
90
+ };
91
+
92
+ /** Reveal the elements with the given ids (as registered via `registerGlobals`). */
93
+ export const revealGlobals = (globalIds: string[]): void => {
94
+ Globals.getInstance().reveal(globalIds);
95
+ };
@@ -75,6 +75,11 @@ describe('HttpBuilder', () => {
75
75
  expect(url.search).toBe('?page=1&limit=10');
76
76
  });
77
77
 
78
+ it('buildRequestUrl leaves an existing query in the endpoint untouched when no new params are passed', () => {
79
+ const url = HttpBuilder.buildRequestUrl('https://api.example.com?page=1');
80
+ expect(url.search).toBe('?page=1');
81
+ });
82
+
78
83
  // Tests for buildRequestOptions
79
84
  it('buildRequestOptions sets GET without body', () => {
80
85
  const options = HttpBuilder.buildRequestOptions(
@@ -106,4 +111,24 @@ describe('HttpBuilder', () => {
106
111
  expect(options.body).toBeUndefined();
107
112
  expect(options.headers).toBeUndefined();
108
113
  });
114
+
115
+ it('buildRequestOptions sets PUT with body', () => {
116
+ const data = { id: 1, name: 'Updated' };
117
+ const options = HttpBuilder.buildRequestOptions('PUT', data, {}, mockAbortSignal);
118
+ expect(options.method).toBe('PUT');
119
+ expect(options.body).toBe(JSON.stringify(data));
120
+ });
121
+
122
+ it('buildRequestOptions sets PATCH with body', () => {
123
+ const data = { name: 'Patched' };
124
+ const options = HttpBuilder.buildRequestOptions('PATCH', data, {}, mockAbortSignal);
125
+ expect(options.method).toBe('PATCH');
126
+ expect(options.body).toBe(JSON.stringify(data));
127
+ });
128
+
129
+ it('buildRequestOptions sets DELETE without body', () => {
130
+ const options = HttpBuilder.buildRequestOptions('DELETE', undefined, {}, mockAbortSignal);
131
+ expect(options.method).toBe('DELETE');
132
+ expect(options.body).toBeUndefined();
133
+ });
109
134
  })
@@ -55,6 +55,26 @@ describe('HttpManager', () => {
55
55
  expect(thrown).toBe(true);
56
56
  });
57
57
 
58
+ it('should throw HttpError with a null response body when the error body is not valid JSON', async () => {
59
+ const mockResponse = new Response('plain text failure', {
60
+ status: 500,
61
+ statusText: 'Internal Server Error',
62
+ headers: { 'Content-Type': 'text/plain' }
63
+ });
64
+
65
+ let thrown = false;
66
+ try {
67
+ await HttpManager.validateResponse(mockResponse);
68
+ } catch (e) {
69
+ thrown = true;
70
+ expect(e).toBeInstanceOf(HttpError);
71
+ expect(e.status).toBe(500);
72
+ expect(e.statusText).toBe('Internal Server Error');
73
+ expect(e.response).toBe(null);
74
+ }
75
+ expect(thrown).toBe(true);
76
+ });
77
+
58
78
  it('should detect JSON content types', () => {
59
79
  expect(HttpManager.isJsonResponse('application/json')).toBe(true);
60
80
  expect(HttpManager.isJsonResponse('application/json; charset=utf-8')).toBe(true);
@@ -204,6 +204,49 @@ describe('HttpClient', () => {
204
204
  expect(lastCall.options.headers['Authorization']).toBe('Bearer custom-token');
205
205
  });
206
206
 
207
+ it('should invoke request interceptors with the final url and options before sending', async () => {
208
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
209
+ const calls = [];
210
+ httpClient.addRequestInterceptor((url, options) => calls.push({ url, options }));
211
+
212
+ await httpClient.get('https://api.example.com/users');
213
+
214
+ expect(calls.length).toBe(1);
215
+ expect(calls[0].url).toBe('https://api.example.com/users');
216
+ expect(calls[0].options.method).toBe('GET');
217
+ });
218
+
219
+ it('should invoke response interceptors with the raw fetch response', async () => {
220
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
221
+ const received = [];
222
+ httpClient.addResponseInterceptor((response) => received.push(response));
223
+
224
+ await httpClient.get('https://api.example.com/users');
225
+
226
+ expect(received.length).toBe(1);
227
+ expect(received[0].ok).toBe(true);
228
+ });
229
+
230
+ it('should propagate a non-timeout error thrown while parsing the response', async () => {
231
+ fetchMock.mockResponse('https://api.example.com/broken', {
232
+ ok: true,
233
+ status: 200,
234
+ statusText: 'OK',
235
+ headers: { get: () => 'application/json' },
236
+ json: () => Promise.reject(new Error('Malformed JSON')),
237
+ text: () => Promise.resolve('not json'),
238
+ });
239
+
240
+ let thrown = false;
241
+ try {
242
+ await httpClient.get('https://api.example.com/broken');
243
+ } catch (error) {
244
+ thrown = true;
245
+ expect(error.message).toBe('Malformed JSON');
246
+ }
247
+ expect(thrown).toBe(true);
248
+ });
249
+
207
250
  it('should handle HTTP errors', async () => {
208
251
  fetchMock.mockError('https://api.example.com/users', 404, { message: 'Not found' });
209
252