@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,145 @@
1
+ import { DomHelper } from '#root/dist/src/core/base-classes/base-component/helpers/dom.helper.js';
2
+
3
+ describe('DomHelper', () => {
4
+ let container;
5
+
6
+ beforeEach(() => {
7
+ container = document.createElement('div');
8
+ container.id = 'mount-target';
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ container = null;
15
+ });
16
+
17
+ it('mountElement appends the element into a string selector target', () => {
18
+ const el = document.createElement('span');
19
+ DomHelper.mountElement(el, '#mount-target');
20
+ expect(container.contains(el)).toBe(true);
21
+ });
22
+
23
+ it('mountElement does nothing when the string selector target is not found', () => {
24
+ const el = document.createElement('span');
25
+ DomHelper.mountElement(el, '#does-not-exist');
26
+ expect(el.parentElement).toBe(null);
27
+ });
28
+
29
+ it('mountElement replaces an HTMLElement target with the element', () => {
30
+ const placeholder = document.createElement('div');
31
+ placeholder.className = 'placeholder';
32
+ container.appendChild(placeholder);
33
+
34
+ const el = document.createElement('span');
35
+ el.className = 'replacement';
36
+ DomHelper.mountElement(el, placeholder);
37
+
38
+ expect(container.contains(placeholder)).toBe(false);
39
+ expect(container.contains(el)).toBe(true);
40
+ });
41
+
42
+ it('createElement creates an element with the given tag name', () => {
43
+ const el = DomHelper.createElement('section');
44
+ expect(el.tagName).toBe('SECTION');
45
+ });
46
+
47
+ it('createElement sets sanitized innerHTML from the template', () => {
48
+ const el = DomHelper.createElement('div', '<p>hi</p><script>alert(1)</script>');
49
+ expect(el.innerHTML).toBe('<p>hi</p>');
50
+ });
51
+
52
+ it('createElement respects the trustLevel when sanitizing', () => {
53
+ const template = '<script>alert(1)</script>';
54
+ const el = DomHelper.createElement('div', template, 'trusted');
55
+ expect(el.innerHTML).toBe(template);
56
+ });
57
+
58
+ it('cleanupOptionalContent removes an optional img with no src', () => {
59
+ container.innerHTML = '<img data-optional>';
60
+ DomHelper.cleanupOptionalContent();
61
+ expect(container.querySelector('img')).toBe(null);
62
+ });
63
+
64
+ it('cleanupOptionalContent keeps an optional img that has a src', () => {
65
+ container.innerHTML = '<img data-optional src="pic.png">';
66
+ DomHelper.cleanupOptionalContent();
67
+ expect(container.querySelector('img')).toBeDefined();
68
+ });
69
+
70
+ it('cleanupOptionalContent removes an optional input with no value', () => {
71
+ container.innerHTML = '<input data-optional>';
72
+ DomHelper.cleanupOptionalContent();
73
+ expect(container.querySelector('input')).toBe(null);
74
+ });
75
+
76
+ it('cleanupOptionalContent keeps an optional input that has a value', () => {
77
+ container.innerHTML = '<input data-optional value="hi">';
78
+ DomHelper.cleanupOptionalContent();
79
+ expect(container.querySelector('input')).toBeDefined();
80
+ });
81
+
82
+ it('cleanupOptionalContent removes an optional textarea with no value', () => {
83
+ container.innerHTML = '<textarea data-optional></textarea>';
84
+ DomHelper.cleanupOptionalContent();
85
+ expect(container.querySelector('textarea')).toBe(null);
86
+ });
87
+
88
+ it('cleanupOptionalContent removes an optional source element with no src', () => {
89
+ container.innerHTML = '<video><source data-optional></video>';
90
+ DomHelper.cleanupOptionalContent();
91
+ expect(container.querySelector('source')).toBe(null);
92
+ });
93
+
94
+ it('cleanupOptionalContent keeps an optional source element that has a src', () => {
95
+ container.innerHTML = '<video><source data-optional src="movie.mp4"></video>';
96
+ DomHelper.cleanupOptionalContent();
97
+ expect(container.querySelector('source')).toBeDefined();
98
+ });
99
+
100
+ it('cleanupOptionalContent removes an optional element with empty textContent', () => {
101
+ container.innerHTML = '<span data-optional> </span>';
102
+ DomHelper.cleanupOptionalContent();
103
+ expect(container.querySelector('span')).toBe(null);
104
+ });
105
+
106
+ it('cleanupOptionalContent removes an optional element whose textContent is the literal string "undefined"', () => {
107
+ container.innerHTML = '<span data-optional>undefined</span>';
108
+ DomHelper.cleanupOptionalContent();
109
+ expect(container.querySelector('span')).toBe(null);
110
+ });
111
+
112
+ it('cleanupOptionalContent keeps an optional element with real textContent', () => {
113
+ container.innerHTML = '<span data-optional>hello</span>';
114
+ DomHelper.cleanupOptionalContent();
115
+ expect(container.querySelector('span')).toBeDefined();
116
+ });
117
+
118
+ it('cleanupOptionalContent removes an element whose data-optional value is the literal string "undefined" or "null"', () => {
119
+ container.innerHTML = `
120
+ <span data-optional="undefined">has content</span>
121
+ <span data-optional="null">has content</span>
122
+ `;
123
+ DomHelper.cleanupOptionalContent();
124
+ expect(container.querySelectorAll('span').length).toBe(0);
125
+ });
126
+
127
+ it('cleanupOptionalContent keeps an element with data-optional="" and non-empty content — a bare/empty data-optional is a plain marker deferred to isEmpty(el), not a forced-removal signal', () => {
128
+ container.innerHTML = '<span data-optional="">has content</span>';
129
+ DomHelper.cleanupOptionalContent();
130
+ expect(container.querySelectorAll('span').length).toBe(1);
131
+ });
132
+
133
+ it('cleanupOptionalContent removes an element whose data-optional value is whitespace-only, even with real content', () => {
134
+ container.innerHTML = '<span data-optional=" ">has content</span>';
135
+ DomHelper.cleanupOptionalContent();
136
+ expect(container.querySelectorAll('span').length).toBe(0);
137
+ });
138
+
139
+ it('cleanupOptionalContent always strips the data-optional attribute from surviving elements', () => {
140
+ container.innerHTML = '<span data-optional>hello</span>';
141
+ DomHelper.cleanupOptionalContent();
142
+ const span = container.querySelector('span');
143
+ expect(span.hasAttribute('data-optional')).toBe(false);
144
+ });
145
+ });
@@ -24,6 +24,46 @@ export class DomHelper {
24
24
  element.innerHTML = SecurityHelper.sanitizeTemplate(template, trustLevel);
25
25
  return element;
26
26
  }
27
+
28
+ public static cleanupOptionalContent(): void {
29
+ const isEmpty = (el: HTMLElement): boolean => {
30
+ if (el instanceof HTMLImageElement) {
31
+ return !el.src || el.src.trim() === "";
32
+ }
33
+
34
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
35
+ return !el.value?.trim();
36
+ }
37
+
38
+ if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
39
+ return !el.getAttribute("src");
40
+ }
41
+
42
+ const content = el.textContent?.trim();
43
+ return !content || content === "undefined";
44
+ };
45
+
46
+ const isValueUndefined = (el: HTMLElement): boolean => {
47
+ const value = el.getAttribute("data-optional");
48
+ /*
49
+ A bare/empty data-optional (getAttribute returns "") is a plain marker and
50
+ defers entirely to isEmpty(el) below rather than forcing removal itself.
51
+ Only a present-but-blank value (whitespace) or the literal string produced
52
+ by interpolating an undefined/null expression counts as "value undefined".
53
+ */
54
+ if (!value) return false;
55
+ return (
56
+ value.trim() === '' ||
57
+ value === "undefined" ||
58
+ value === "null"
59
+ );
60
+ }
61
+
62
+ document.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
63
+ if (isValueUndefined(el) || isEmpty(el)) el.remove();
64
+ el.removeAttribute("data-optional");
65
+ });
66
+ }
27
67
 
28
68
  private static appendOrReplace(config: DomElementConfig) {
29
69
  if (config.target instanceof HTMLElement) {
@@ -35,7 +75,7 @@ export class DomHelper {
35
75
  config.target.replaceWith(config.element);
36
76
  }
37
77
  } else {
38
- console.warn('Mount target not found:', config.mountTarget);
78
+ return;
39
79
  }
40
80
  }
41
81
  }
@@ -0,0 +1,88 @@
1
+ import { EventHelper } from '#root/dist/src/core/base-classes/base-component/helpers/event.helper.js';
2
+
3
+ describe('EventHelper', () => {
4
+ let container;
5
+ let eventListeners;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ eventListeners = [];
11
+ });
12
+
13
+ afterEach(() => {
14
+ container.remove();
15
+ container = null;
16
+ });
17
+
18
+ it('binds a handler resolved from data-event and calls it with `this` set to the component', () => {
19
+ container.innerHTML = '<button data-event="click:handleClick"></button>';
20
+ const button = container.querySelector('button');
21
+
22
+ let calledWith = null;
23
+ const component = {
24
+ handleClick(...args) {
25
+ calledWith = { self: this, args };
26
+ },
27
+ };
28
+
29
+ EventHelper.bindEvents(component, container, eventListeners);
30
+ click(button);
31
+
32
+ expect(calledWith.self).toBe(component);
33
+ expect(calledWith.args.length).toBe(0);
34
+ });
35
+
36
+ it('resolves comma-separated raw args via TokenHelper before invoking the handler', () => {
37
+ container.innerHTML = '<button data-event="click:handleClick:1,\'hi\'" id="btn"></button>';
38
+ const button = container.querySelector('button');
39
+
40
+ let received = null;
41
+ const component = {
42
+ handleClick(...args) { received = args; },
43
+ };
44
+
45
+ EventHelper.bindEvents(component, container, eventListeners);
46
+ click(button);
47
+
48
+ expect(received[0]).toBe(1);
49
+ expect(received[1]).toBe('hi');
50
+ });
51
+
52
+ it('does nothing when the handler name is missing', () => {
53
+ container.innerHTML = '<button data-event="click:"></button>';
54
+ const component = {};
55
+ EventHelper.bindEvents(component, container, eventListeners);
56
+ expect(eventListeners.length).toBe(0);
57
+ });
58
+
59
+ it('does nothing when the referenced handler does not exist or is not a function', () => {
60
+ container.innerHTML = '<button data-event="click:missingHandler"></button>';
61
+ const component = { missingHandler: 'not-a-function' };
62
+ EventHelper.bindEvents(component, container, eventListeners);
63
+ expect(eventListeners.length).toBe(0);
64
+ });
65
+
66
+ it('tracks each bound listener in the eventListeners array', () => {
67
+ container.innerHTML = '<button data-event="click:handleClick"></button>';
68
+ const component = { handleClick() {} };
69
+
70
+ EventHelper.bindEvents(component, container, eventListeners);
71
+
72
+ expect(eventListeners.length).toBe(1);
73
+ expect(eventListeners[0][1]).toBe('click');
74
+ });
75
+
76
+ it('destroyEvents removes every tracked listener', () => {
77
+ container.innerHTML = '<button data-event="click:handleClick"></button>';
78
+ const button = container.querySelector('button');
79
+ let callCount = 0;
80
+ const component = { handleClick() { callCount++; } };
81
+
82
+ EventHelper.bindEvents(component, container, eventListeners);
83
+ EventHelper.destroyEvents(eventListeners);
84
+
85
+ click(button);
86
+ expect(callCount).toBe(0);
87
+ });
88
+ });
@@ -3,13 +3,13 @@ import { TokenHelper } from './token.helper.js';
3
3
 
4
4
  export class EventHelper {
5
5
  public static bindEvents(
6
- component: BaseComponent,
7
- element: HTMLElement,
8
- eventListeners: Array<[string, EventListener]>
6
+ component: BaseComponent,
7
+ element: HTMLElement,
8
+ eventListeners: Array<[EventTarget, string, EventListener]>
9
9
  ): void {
10
10
  element.querySelectorAll('[data-event]').forEach(el => {
11
- const parts = el.getAttribute('data-event')!.split(':');
12
- const [eventName, handlerName, argsString] = parts;
11
+ const [eventName, handlerName, ...argParts] = el.getAttribute('data-event')!.split(':');
12
+ const argsString = argParts.length ? argParts.join(':') : '';
13
13
 
14
14
  if (!handlerName || !eventName) return;
15
15
 
@@ -24,11 +24,10 @@ export class EventHelper {
24
24
  }
25
25
 
26
26
  public static destroyEvents(
27
- element: HTMLElement,
28
- eventListeners: Array<[string, EventListener]>
27
+ eventListeners: Array<[EventTarget, string, EventListener]>
29
28
  ): void {
30
- eventListeners.forEach(([event, listener]) => {
31
- element.removeEventListener(event, listener);
29
+ eventListeners.forEach(([target, event, listener]) => {
30
+ target.removeEventListener(event, listener);
32
31
  });
33
32
  }
34
33
 
@@ -46,12 +45,12 @@ export class EventHelper {
46
45
  }
47
46
 
48
47
  private static addEvent(
49
- target: EventTarget,
50
- event: string,
51
- listener: EventListener,
52
- eventListeners: Array<[string, EventListener]>
48
+ target: EventTarget,
49
+ event: string,
50
+ listener: EventListener,
51
+ eventListeners: Array<[EventTarget, string, EventListener]>
53
52
  ): void {
54
53
  target.addEventListener(event, listener);
55
- eventListeners.push([event, listener]);
54
+ eventListeners.push([target, event, listener]);
56
55
  }
57
56
  }
@@ -0,0 +1,62 @@
1
+ import { I18nHelper } from '#root/dist/src/core/base-classes/base-component/helpers/i18n.helper.js';
2
+ import { I18nService } from '#root/dist/src/core/services/index.js';
3
+
4
+ describe('I18nHelper', () => {
5
+ let container;
6
+ let originalTranslate;
7
+
8
+ beforeEach(() => {
9
+ container = document.createElement('div');
10
+ document.body.appendChild(container);
11
+ originalTranslate = I18nService.translate;
12
+ });
13
+
14
+ afterEach(() => {
15
+ I18nService.translate = originalTranslate;
16
+ container.remove();
17
+ container = null;
18
+ });
19
+
20
+ it('sets the placeholder of an input element to the translated value', () => {
21
+ I18nService.translate = (key) => key === 'greeting' ? 'Hello' : key;
22
+ container.innerHTML = '<input data-i18n="greeting">';
23
+
24
+ I18nHelper.parseI18nAttributes(container);
25
+
26
+ const input = container.querySelector('input');
27
+ expect(input.placeholder).toBe('Hello');
28
+ });
29
+
30
+ it('sets the textContent of a non-input element to the translated value', () => {
31
+ I18nService.translate = (key) => key === 'greeting' ? 'Hello' : key;
32
+ container.innerHTML = '<span data-i18n="greeting"></span>';
33
+
34
+ I18nHelper.parseI18nAttributes(container);
35
+
36
+ const span = container.querySelector('span');
37
+ expect(span.textContent).toBe('Hello');
38
+ });
39
+
40
+ it('passes the element textContent as the fallback to translate', () => {
41
+ const calls = [];
42
+ I18nService.translate = (key, fallback) => { calls.push({ key, fallback }); return fallback; };
43
+ container.innerHTML = '<span data-i18n="missing.key">fallback text</span>';
44
+
45
+ I18nHelper.parseI18nAttributes(container);
46
+
47
+ expect(calls[0].key).toBe('missing.key');
48
+ expect(calls[0].fallback).toBe('fallback text');
49
+ expect(container.querySelector('span').textContent).toBe('fallback text');
50
+ });
51
+
52
+ it('processes every element with a data-i18n attribute', () => {
53
+ I18nService.translate = (key) => `[${key}]`;
54
+ container.innerHTML = '<span data-i18n="a"></span><span data-i18n="b"></span>';
55
+
56
+ I18nHelper.parseI18nAttributes(container);
57
+
58
+ const spans = container.querySelectorAll('span');
59
+ expect(spans[0].textContent).toBe('[a]');
60
+ expect(spans[1].textContent).toBe('[b]');
61
+ });
62
+ });
@@ -0,0 +1,82 @@
1
+ import { PipeHelper } from '#root/dist/src/core/base-classes/base-component/helpers/pipe.helper.js';
2
+ import { AppPipeRegistry } from '#root/dist/src/core/services/index.js';
3
+
4
+ describe('PipeHelper', () => {
5
+ let container;
6
+
7
+ beforeAll(() => {
8
+ AppPipeRegistry.register('upper', (value) => String(value).toUpperCase());
9
+ AppPipeRegistry.register('suffix', (value, suffix) => `${value}${suffix}`);
10
+ AppPipeRegistry.register('join', (value, a, b) => `${value}-${a}-${b}`);
11
+ });
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ container.remove();
20
+ container = null;
21
+ });
22
+
23
+ it('reads from textContent and writes the piped result back to textContent for a plain element', () => {
24
+ container.innerHTML = '<span data-pipe="upper">hi</span>';
25
+ PipeHelper.parsePipeAttributes(container);
26
+ expect(container.querySelector('span').textContent).toBe('HI');
27
+ });
28
+
29
+ it('reads from .value and writes the piped result back to .value for an input element', () => {
30
+ container.innerHTML = '<input data-pipe="upper" value="hi">';
31
+ PipeHelper.parsePipeAttributes(container);
32
+ expect(container.querySelector('input').value).toBe('HI');
33
+ });
34
+
35
+ it('reads from .value for a textarea element', () => {
36
+ container.innerHTML = '<textarea data-pipe="upper">hi</textarea>';
37
+ PipeHelper.parsePipeAttributes(container);
38
+ expect(container.querySelector('textarea').value).toBe('HI');
39
+ });
40
+
41
+ it('prefers data-pipe-source over the element value when present', () => {
42
+ container.innerHTML = '<span data-pipe="upper" data-pipe-source="from source">ignored</span>';
43
+ PipeHelper.parsePipeAttributes(container);
44
+ expect(container.querySelector('span').textContent).toBe('FROM SOURCE');
45
+ });
46
+
47
+ it('chains multiple pipes separated by |', () => {
48
+ container.innerHTML = '<span data-pipe="upper|suffix:!">hi</span>';
49
+ PipeHelper.parsePipeAttributes(container);
50
+ expect(container.querySelector('span').textContent).toBe('HI!');
51
+ });
52
+
53
+ it('passes comma-separated arguments to the pipe function', () => {
54
+ container.innerHTML = '<span data-pipe="join:x,y">hi</span>';
55
+ PipeHelper.parsePipeAttributes(container);
56
+ expect(container.querySelector('span').textContent).toBe('hi-x-y');
57
+ });
58
+
59
+ it('does nothing when the element has no data-pipe attribute', () => {
60
+ container.innerHTML = '<span>hi</span>';
61
+ PipeHelper.parsePipeAttributes(container);
62
+ expect(container.querySelector('span').textContent).toBe('hi');
63
+ });
64
+
65
+ it('does nothing when the data-pipe attribute is empty', () => {
66
+ container.innerHTML = '<span data-pipe="">hi</span>';
67
+ PipeHelper.parsePipeAttributes(container);
68
+ expect(container.querySelector('span').textContent).toBe('hi');
69
+ });
70
+
71
+ it('prefers data-pipe-source over .value for an input element', () => {
72
+ container.innerHTML = '<input data-pipe="upper" data-pipe-source="from source" value="ignored">';
73
+ PipeHelper.parsePipeAttributes(container);
74
+ expect(container.querySelector('input').value).toBe('FROM SOURCE');
75
+ });
76
+
77
+ it('stops processing pipes when a pipe segment resolves to an empty name', () => {
78
+ container.innerHTML = '<span data-pipe=":arg">hi</span>';
79
+ PipeHelper.parsePipeAttributes(container);
80
+ expect(container.querySelector('span').textContent).toBe('hi');
81
+ });
82
+ });
@@ -0,0 +1,142 @@
1
+ import { SecurityHelper } from '#root/dist/src/core/base-classes/base-component/helpers/security.helper.js';
2
+
3
+ describe('SecurityHelper', () => {
4
+ it('sanitizeTemplate returns an empty string for null and undefined', () => {
5
+ expect(SecurityHelper.sanitizeTemplate(null)).toBe('');
6
+ expect(SecurityHelper.sanitizeTemplate(undefined)).toBe('');
7
+ });
8
+
9
+ it('sanitizeTemplate stringifies non-string values before sanitizing', () => {
10
+ expect(SecurityHelper.sanitizeTemplate(42)).toBe('42');
11
+ expect(SecurityHelper.sanitizeTemplate(true)).toBe('true');
12
+ });
13
+
14
+ it('sanitizeTemplate defaults to the "normal" trust level', () => {
15
+ const result = SecurityHelper.sanitizeTemplate('<div><script>alert(1)</script></div>');
16
+ expect(result).toBe('<div></div>');
17
+ });
18
+
19
+ it('sanitizeTemplate "trusted" level leaves the template completely unchanged', () => {
20
+ const template = '<script>alert(1)</script><div onclick="x()">hi</div>';
21
+ expect(SecurityHelper.sanitizeTemplate(template, 'trusted')).toBe(template);
22
+ });
23
+
24
+ it('sanitizeTemplate "normal" level strips <script> tags', () => {
25
+ const result = SecurityHelper.sanitizeTemplate('<p>hi</p><script>alert(1)</script>', 'normal');
26
+ expect(result).toBe('<p>hi</p>');
27
+ });
28
+
29
+ it('sanitizeTemplate "normal" level strips inline on* event handlers', () => {
30
+ const result = SecurityHelper.sanitizeTemplate('<button onclick="doBad()">Click</button>', 'normal');
31
+ expect(result).toContain('<button');
32
+ expect(result.includes('onclick')).toBe(false);
33
+ });
34
+
35
+ it('sanitizeTemplate "normal" level keeps iframes (only "strict" removes them)', () => {
36
+ const result = SecurityHelper.sanitizeTemplate('<iframe src="https://example.com"></iframe>', 'normal');
37
+ expect(result).toContain('<iframe');
38
+ });
39
+
40
+ it('sanitizeTemplate "strict" level also strips iframe, object and embed tags', () => {
41
+ const result = SecurityHelper.sanitizeTemplate(
42
+ '<iframe src="x"></iframe><object data="x"></object><embed src="x">',
43
+ 'strict'
44
+ );
45
+ expect(result.includes('<iframe')).toBe(false);
46
+ expect(result.includes('<object')).toBe(false);
47
+ expect(result.includes('<embed')).toBe(false);
48
+ });
49
+
50
+ it('sanitizeTemplate "strict" level strips javascript: hrefs and data: srcs', () => {
51
+ const result = SecurityHelper.sanitizeTemplate(
52
+ '<a href="javascript:alert(1)">link</a><img src="data:text/html,evil">',
53
+ 'strict'
54
+ );
55
+ expect(result.includes('javascript:')).toBe(false);
56
+ expect(result.includes('src="data:')).toBe(false);
57
+ });
58
+
59
+ it('sanitizeTemplate "strict" level still strips scripts and inline handlers like normal', () => {
60
+ const result = SecurityHelper.sanitizeTemplate(
61
+ '<script>bad()</script><div onmouseover="bad()">hi</div>',
62
+ 'strict'
63
+ );
64
+ expect(result.includes('<script>')).toBe(false);
65
+ expect(result.includes('onmouseover')).toBe(false);
66
+ });
67
+
68
+ it('sanitizeTemplate "normal" level strips unquoted event handler attributes', () => {
69
+ const result = SecurityHelper.sanitizeTemplate('<img src=x onerror=alert(1)>', 'normal');
70
+ expect(result.includes('onerror')).toBe(false);
71
+ });
72
+
73
+ it('sanitizeTemplate "normal" level is not fooled by nested-tag script reassembly', () => {
74
+ const result = SecurityHelper.sanitizeTemplate(
75
+ '<scr<script></script>ipt>alert(document.domain)</scr<script></script>ipt>',
76
+ 'normal'
77
+ );
78
+ // A naive substring check is misleading here: the sanitized output can still
79
+ // contain the literal text "<script>" as part of an inert malformed tag name
80
+ // (e.g. "SCR<SCRIPT"), which HTML parsers never re-interpret as a real <script>
81
+ // element. Re-parse the result and assert no live <script> element exists.
82
+ const reparsed = document.createElement('template');
83
+ reparsed.innerHTML = result;
84
+ expect(reparsed.content.querySelector('script')).toBe(null);
85
+ });
86
+
87
+ it('sanitizeTemplate "normal" level is not fooled by nested-tag event handler reassembly', () => {
88
+ const result = SecurityHelper.sanitizeTemplate(
89
+ '<img src=x onerr<script>x</script>or=alert(1)>',
90
+ 'normal'
91
+ );
92
+ expect(result.includes('onerror')).toBe(false);
93
+ });
94
+
95
+ it('sanitizeTemplate "strict" level strips whitespace-obfuscated javascript: URLs', () => {
96
+ const result = SecurityHelper.sanitizeTemplate('<a href="java\tscript:alert(1)">x</a>', 'strict');
97
+ expect(result.includes('javascript:')).toBe(false);
98
+ });
99
+
100
+ it('sanitizeTemplate "strict" level strips javascript: from action/formaction/poster/background too', () => {
101
+ const result = SecurityHelper.sanitizeTemplate('<form action="javascript:alert(1)">x</form>', 'strict');
102
+ expect(result.includes('javascript:')).toBe(false);
103
+ });
104
+
105
+ it('sanitizeInputElement escapes the value of an input element', () => {
106
+ const input = document.createElement('input');
107
+ input.value = '<script>alert(1)</script>';
108
+ expect(SecurityHelper.sanitizeInputElement(input)).toBe('&lt;script&gt;alert(1)&lt;/script&gt;');
109
+ });
110
+
111
+ it('sanitizeInputElement escapes the value of a textarea element', () => {
112
+ const textarea = document.createElement('textarea');
113
+ textarea.value = '<b>bold</b>';
114
+ expect(SecurityHelper.sanitizeInputElement(textarea)).toBe('&lt;b&gt;bold&lt;/b&gt;');
115
+ });
116
+
117
+ it('sanitizeInputElement escapes innerText for contenteditable elements', () => {
118
+ const div = document.createElement('div');
119
+ div.setAttribute('contenteditable', 'true');
120
+ div.innerText = '<i>hi</i>';
121
+ expect(SecurityHelper.sanitizeInputElement(div)).toBe('&lt;i&gt;hi&lt;/i&gt;');
122
+ });
123
+
124
+ it('sanitizeInputElement returns an empty string for a plain, non-editable element', () => {
125
+ const div = document.createElement('div');
126
+ expect(SecurityHelper.sanitizeInputElement(div)).toBe('');
127
+ });
128
+
129
+ it('escapeHtml escapes all special characters', () => {
130
+ expect(SecurityHelper.escapeHtml(`<div class="a" data='b'>&</div>`))
131
+ .toBe('&lt;div class=&quot;a&quot; data=&#039;b&#039;&gt;&amp;&lt;/div&gt;');
132
+ });
133
+
134
+ it('escapeHtml returns an empty string for null and undefined', () => {
135
+ expect(SecurityHelper.escapeHtml(null)).toBe('');
136
+ expect(SecurityHelper.escapeHtml(undefined)).toBe('');
137
+ });
138
+
139
+ it('escapeHtml stringifies non-string values before escaping', () => {
140
+ expect(SecurityHelper.escapeHtml(123)).toBe('123');
141
+ });
142
+ });