@nutin/cli 2.1.1 → 3.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 (229) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/README.md +13 -5
  3. package/bin/add-feature.mjs +1 -1
  4. package/bin/cli.mjs +1 -1
  5. package/bin/create-app.mjs +1 -1
  6. package/bin/update-app.mjs +1 -1
  7. package/lib/index.mjs +6 -2
  8. package/lib/src/add-feature/base-template-patcher.mjs +139 -0
  9. package/lib/src/add-feature/feature-adder.mjs +20 -9
  10. package/lib/src/add-feature/feature-context-builder.mjs +1 -0
  11. package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
  12. package/lib/src/add-feature/package-json-updater.mjs +7 -9
  13. package/lib/src/common/feature-registry.mjs +4 -0
  14. package/lib/src/common/file-generator.mjs +25 -3
  15. package/lib/src/common/git.mjs +31 -0
  16. package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
  17. package/lib/src/common/package-json-helper.mjs +14 -4
  18. package/lib/src/common/project-meta.mjs +2 -2
  19. package/lib/src/common/template-compiler.mjs +2 -0
  20. package/lib/src/common/transpile-ts.mjs +94 -0
  21. package/lib/src/create-app/context-builder.mjs +1 -0
  22. package/lib/src/create-app/git-manager.mjs +12 -2
  23. package/lib/src/create-app/json-manager.mjs +21 -18
  24. package/lib/src/create-app/new-app-prompt.mjs +4 -2
  25. package/lib/src/create-app/success-message.mjs +1 -1
  26. package/lib/src/update-app/conflict-report-writer.mjs +33 -21
  27. package/lib/src/update-app/json-drift.mjs +54 -0
  28. package/lib/src/update-app/project-updater.mjs +123 -14
  29. package/lib/src/update-app/template-differ.mjs +5 -5
  30. package/lib/src/update-app/three-way-merge.mjs +42 -0
  31. package/lib/src/update-app/update-context-builder.mjs +1 -0
  32. package/lib/src/update-app/update-summary-printer.mjs +10 -2
  33. package/package.json +9 -8
  34. package/templates/base/AGENTS.md.hbs +42 -41
  35. package/templates/base/GETTING_STARTED.md.hbs +26 -20
  36. package/templates/base/README.md.hbs +7 -4
  37. package/templates/base/src/app/globals.d.ts.hbs +6 -3
  38. package/templates/base/src/app/guards.ts.hbs +1 -3
  39. package/templates/base/src/app/main.ts.hbs +2 -9
  40. package/templates/base/src/app/routes.ts.hbs +7 -2
  41. package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
  42. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  43. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
  44. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
  45. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
  46. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
  47. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
  48. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +286 -24
  49. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
  50. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +66 -4
  51. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
  52. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
  53. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +93 -54
  54. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
  55. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
  57. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
  58. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
  59. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
  60. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
  61. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
  62. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
  63. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
  64. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
  65. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
  66. package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
  67. package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
  68. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
  69. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
  70. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
  71. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
  72. package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
  73. package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
  74. package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
  75. package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
  76. package/templates/base/src/core/index.ts.hbs +4 -2
  77. package/templates/base/src/core/internals.d.ts.hbs +199 -20
  78. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
  79. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
  80. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
  81. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
  82. package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
  83. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
  84. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
  85. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
  86. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
  87. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
  88. package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
  89. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
  90. package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
  91. package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
  92. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
  93. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
  94. package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
  95. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +183 -4
  96. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
  97. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
  98. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
  99. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
  100. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
  101. package/templates/base/src/core/services/router/router.test.js.hbs +306 -0
  102. package/templates/base/src/core/services/router/router.ts.hbs +113 -53
  103. package/templates/base/tools/builder/builder.config.js.hbs +6 -0
  104. package/templates/base/tools/builder/builder.js.hbs +18 -6
  105. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
  106. package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
  107. package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
  108. package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
  109. package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
  110. package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
  111. package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
  112. package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
  113. package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
  114. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
  115. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
  116. package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
  117. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
  118. package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
  119. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
  120. package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
  121. package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
  122. package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
  123. package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
  124. package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
  125. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
  126. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +15 -7
  127. package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
  128. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
  129. package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
  130. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
  131. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +43 -13
  132. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
  133. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
  134. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +28 -21
  135. package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
  136. package/templates/base/tools/builder/core/seo/route-output.js.hbs +24 -6
  137. package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
  138. package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
  139. package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
  140. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
  141. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
  142. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
  143. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +24 -11
  144. package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
  145. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
  146. package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
  147. package/templates/base/tools/dev/serve.js.hbs +152 -22
  148. package/templates/base/tools/dev/watcher.js.hbs +34 -3
  149. package/templates/base/tools/generator/generator.js.hbs +1 -1
  150. package/templates/base/tools/generator/handle-file.js.hbs +4 -2
  151. package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
  152. package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
  153. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
  154. package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
  155. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
  156. package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
  157. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  158. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
  159. package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
  160. package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
  161. package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
  162. package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
  163. package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
  164. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
  165. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
  166. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
  167. package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
  168. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
  169. package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
  170. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
  171. package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
  172. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
  173. package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
  174. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
  175. package/templates/base/tools/utils/build-lock.js.hbs +30 -2
  176. package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
  177. package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
  178. package/templates/base/tools/utils/format.js.hbs +1 -1
  179. package/templates/base/tools/utils/index.js.hbs +2 -1
  180. package/templates/base/tools/utils/print.js.hbs +2 -2
  181. package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
  182. package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
  183. package/templates/base/tools/utils/run-command.js.hbs +30 -1
  184. package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
  185. package/templates/base/tools/utils/run-script.js.hbs +2 -2
  186. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +3 -0
  187. package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
  188. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
  189. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
  190. package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
  191. package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
  192. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
  193. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
  194. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
  195. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
  196. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +21 -4
  197. package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
  198. package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
  199. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
  200. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
  201. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
  202. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
  203. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
  204. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
  205. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
  206. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
  207. package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
  208. package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
  209. package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
  210. package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
  211. package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
  212. package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
  213. package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
  214. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
  215. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
  216. package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
  217. package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
  218. package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
  219. package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
  220. package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
  221. package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
  222. package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
  223. package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
  224. package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
  225. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
  226. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
  227. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
  228. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
  229. /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
@@ -20,7 +20,6 @@ export function addTest(test) {
20
20
 
21
21
  export async function runQueuedTests() {
22
22
  printStart();
23
- const startTime = performance.now();
24
23
  const testAmount = testQueue.length;
25
24
  const results = [];
26
25
  let previousSuiteKey = null;
@@ -70,10 +69,10 @@ export async function runQueuedTests() {
70
69
 
71
70
  previousSuiteKey = suiteKey(test);
72
71
  previousSuite = test.suiteName;
73
- previousSuiteAfterAll = test.afterAll;
74
- if (test.beforeAll) {
72
+ previousSuiteAfterAll = test.hooks?.afterAll;
73
+ if (test.hooks?.beforeAll) {
75
74
  try {
76
- await test.beforeAll();
75
+ await test.hooks.beforeAll();
77
76
  } catch (err) {
78
77
  failed++;
79
78
  results.push({
@@ -99,7 +98,7 @@ export async function runQueuedTests() {
99
98
 
100
99
  let testError = null;
101
100
  try {
102
- if (test.beforeEach) await test.beforeEach();
101
+ if (test.hooks?.beforeEach) await test.hooks.beforeEach();
103
102
  await test.testFn();
104
103
  } catch (err) {
105
104
  testError = err;
@@ -107,7 +106,7 @@ export async function runQueuedTests() {
107
106
 
108
107
  let afterEachError = null;
109
108
  try {
110
- if (test.afterEach) await test.afterEach();
109
+ if (test.hooks?.afterEach) await test.hooks.afterEach();
111
110
  } catch (err) {
112
111
  afterEachError = err;
113
112
  }
@@ -145,8 +144,9 @@ export async function runQueuedTests() {
145
144
  teardownJsdom();
146
145
  }
147
146
 
148
- const endTime = performance.now();
149
- elapsedTime = (endTime - startTime);
147
+ // Since the process started (performance's time origin): Node startup, discovery and
148
+ // the test files' imports count too, not only running the queued tests.
149
+ elapsedTime = performance.now();
150
150
  printResults(results);
151
151
  printSummary(todo, passed, failed, elapsedTime);
152
152
 
@@ -1,45 +1,47 @@
1
1
  import { createMockMethod } from './create-mock-method.js';
2
2
 
3
+ // Mirrors EventBus's public API (subscribe/once/emit/off): every method is a trackable
4
+ // mock whose default implementation really dispatches, so subscribed callbacks run.
3
5
  export class MockEventBus {
4
6
  constructor() {
5
7
  this.handlers = {};
6
8
 
7
- this.subscribe = createMockMethod();
8
- this.emit = (...args) => {
9
- this.emit.calls.push(args);
10
- const [event, data] = args;
11
- const callbacks = this.handlers[event];
12
- if (callbacks) {
13
- callbacks.forEach(cb => cb(data));
14
- }
15
- };
16
- this.emit.calls = [];
17
- this.emit.mockReset = () => { this.emit.calls = []; };
18
-
19
- this.off = (event, callback) => {
20
- this.off.calls.push([event, callback]);
21
- if (!this.handlers[event]) return;
22
- this.handlers[event] = this.handlers[event].filter(cb => cb !== callback);
23
- };
24
- this.off.calls = [];
25
- this.off.mockReset = () => { this.off.calls = []; };
26
-
27
- this.on = createMockMethod((event, callback) => {
28
- if (!this.handlers[event]) this.handlers[event] = [];
29
- this.handlers[event].push(callback);
9
+ this.subscribe = createMockMethod((event, callback) => this._addHandler(event, callback, false));
10
+ this.once = createMockMethod((event, callback) => this._addHandler(event, callback, true));
11
+
12
+ this.emit = createMockMethod((event, data) => {
13
+ const entries = this.handlers[event];
14
+ if (!entries) return;
15
+ entries.slice().forEach((entry) => {
16
+ if (entry.once) this._removeHandler(event, entry.callback);
17
+ entry.callback(data);
18
+ });
19
+ });
20
+
21
+ this.off = createMockMethod((event, callback) => {
22
+ if (callback) this._removeHandler(event, callback);
23
+ else delete this.handlers[event];
30
24
  });
31
25
 
32
26
  this.onDestroy = createMockMethod();
33
27
  this.reset = this.reset.bind(this);
34
28
  }
35
29
 
30
+ _addHandler(event, callback, once) {
31
+ (this.handlers[event] ??= []).push({ callback, once });
32
+ }
33
+
34
+ _removeHandler(event, callback) {
35
+ if (!this.handlers[event]) return;
36
+ this.handlers[event] = this.handlers[event].filter((entry) => entry.callback !== callback);
37
+ }
38
+
36
39
  reset() {
37
40
  this.subscribe.mockReset();
41
+ this.once.mockReset();
38
42
  this.emit.mockReset();
39
43
  this.off.mockReset();
40
- this.on.mockReset();
41
44
  this.onDestroy.mockReset();
42
45
  this.handlers = {};
43
46
  }
44
47
  }
45
-
@@ -1,10 +1,10 @@
1
1
  import { MockEventBus } from './mock-event-bus.js';
2
2
 
3
3
  describe('MockEventBus', () => {
4
- it('on() + emit() dispatches the event data to the registered handler', () => {
4
+ it('subscribe() + emit() dispatches the event data to the subscribed callback', () => {
5
5
  const bus = new MockEventBus();
6
6
  let received;
7
- bus.on('greet', (data) => { received = data; });
7
+ bus.subscribe('greet', (data) => { received = data; });
8
8
 
9
9
  bus.emit('greet', 'hello');
10
10
 
@@ -12,53 +12,61 @@ describe('MockEventBus', () => {
12
12
  expect(bus.emit.calls).toEqual([['greet', 'hello']]);
13
13
  });
14
14
 
15
- it('on() is a trackable mock', () => {
15
+ it('subscribe() is a trackable mock', () => {
16
16
  const bus = new MockEventBus();
17
17
  const handler = () => {};
18
- bus.on('greet', handler);
18
+ bus.subscribe('greet', handler);
19
19
 
20
- expect(bus.on).toHaveBeenCalled();
21
- expect(bus.on).toHaveBeenCalledWith('greet', handler);
20
+ expect(bus.subscribe).toHaveBeenCalledWith('greet', handler);
22
21
  });
23
22
 
24
- it('off() removes a previously registered handler', () => {
23
+ it('once() callbacks run for the first emit only', () => {
25
24
  const bus = new MockEventBus();
26
25
  let callCount = 0;
27
- const handler = () => { callCount++; };
28
- bus.on('greet', handler);
29
- bus.off('greet', handler);
26
+ bus.once('greet', () => { callCount++; });
30
27
 
31
- bus.emit('greet', 'hello');
28
+ bus.emit('greet');
29
+ bus.emit('greet');
32
30
 
33
- expect(callCount).toBe(0);
31
+ expect(callCount).toBe(1);
34
32
  });
35
33
 
36
- it('subscribe() is a bare mock and does not actually register a handler', () => {
34
+ it('off(event, callback) removes that callback only', () => {
37
35
  const bus = new MockEventBus();
38
- let called = false;
39
- const handler = () => { called = true; };
36
+ let removed = 0;
37
+ let kept = 0;
38
+ const handler = () => { removed++; };
40
39
  bus.subscribe('greet', handler);
40
+ bus.subscribe('greet', () => { kept++; });
41
+ bus.off('greet', handler);
41
42
 
42
43
  bus.emit('greet', 'hello');
43
44
 
44
- expect(called).toBeFalsy();
45
- expect(bus.subscribe.calls.length).toBe(1);
46
- expect(bus.subscribe.calls[0][0]).toBe('greet');
47
- expect(bus.subscribe.calls[0][1]).toBe(handler);
45
+ expect(removed).toBe(0);
46
+ expect(kept).toBe(1);
47
+ });
48
+
49
+ it('off(event) removes every callback for that event', () => {
50
+ const bus = new MockEventBus();
51
+ let callCount = 0;
52
+ bus.subscribe('greet', () => { callCount++; });
53
+ bus.off('greet');
54
+
55
+ bus.emit('greet');
56
+
57
+ expect(callCount).toBe(0);
48
58
  });
49
59
 
50
60
  it('reset() clears handlers and call logs', () => {
51
61
  const bus = new MockEventBus();
52
62
  let callCount = 0;
53
- bus.on('greet', () => { callCount++; });
63
+ bus.subscribe('greet', () => { callCount++; });
54
64
  bus.emit('greet', 'x');
55
- bus.subscribe('greet', () => {});
56
65
 
57
66
  bus.reset();
58
67
 
59
68
  expect(bus.emit.calls).toEqual([]);
60
69
  expect(bus.subscribe.calls).toEqual([]);
61
- expect(bus.on.calls).toEqual([]);
62
70
 
63
71
  bus.emit('greet', 'y');
64
72
  expect(callCount).toBe(1); // only the pre-reset emit reached the handler
@@ -1,22 +1,19 @@
1
1
  import { createMockMethod } from './create-mock-method.js';
2
2
 
3
+ const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'addRequestInterceptor', 'addResponseInterceptor', 'onDestroy'];
4
+
5
+ // Mirrors HttpClient's public API; every method is a bare mock (configure responses
6
+ // with e.g. `client.get.mockReturnValue(Promise.resolve(data))`).
3
7
  export class MockHttpClient {
4
8
  constructor() {
5
- this._defineHttpMethods();
6
- this.onDestroy = createMockMethod();
7
- this.reset = this.reset.bind(this);
8
- }
9
-
10
- _defineHttpMethods() {
11
- const methods = ['get', 'post', 'put', 'patch', 'delete'];
12
- for (const method of methods) {
9
+ for (const method of METHODS) {
13
10
  this[method] = createMockMethod();
14
11
  }
12
+ this.reset = this.reset.bind(this);
15
13
  }
16
14
 
17
15
  reset() {
18
- const methods = ['get', 'post', 'put', 'patch', 'delete', 'onDestroy'];
19
- for (const method of methods) {
16
+ for (const method of METHODS) {
20
17
  this[method].mockReset();
21
18
  }
22
19
  }
@@ -11,16 +11,28 @@ describe('MockHttpClient', () => {
11
11
  expect(client.put.calls).toEqual([]);
12
12
  });
13
13
 
14
+ it('exposes the interceptor registration methods as mocks', () => {
15
+ const client = new MockHttpClient();
16
+ const interceptor = () => {};
17
+ client.addRequestInterceptor(interceptor);
18
+ client.addResponseInterceptor(interceptor);
19
+
20
+ expect(client.addRequestInterceptor).toHaveBeenCalledWith(interceptor);
21
+ expect(client.addResponseInterceptor).toHaveBeenCalledWith(interceptor);
22
+ });
23
+
14
24
  it('reset() clears calls and configured return values on every method', () => {
15
25
  const client = new MockHttpClient();
16
26
  client.get.mockReturnValue('cached');
17
27
  client.get();
18
28
  client.delete('/widgets/1');
29
+ client.addRequestInterceptor(() => {});
19
30
 
20
31
  client.reset();
21
32
 
22
33
  expect(client.get.calls).toEqual([]);
23
34
  expect(client.delete.calls).toEqual([]);
35
+ expect(client.addRequestInterceptor.calls).toEqual([]);
24
36
  expect(client.get()).toBe(undefined);
25
37
  });
26
38
  });
@@ -1,5 +1,7 @@
1
1
  import { createMockMethod } from './create-mock-method.js';
2
2
 
3
+ // Mirrors I18n's public API with in-memory translations (seed them with
4
+ // setTranslations()/setDefaultTranslations()); no fetch, no localStorage.
3
5
  export class MockI18n {
4
6
  constructor(defaultLang = 'en', supportedLangs = ['en']) {
5
7
  this._DEFAULT_LANGUAGE = defaultLang;
@@ -7,39 +9,41 @@ export class MockI18n {
7
9
  this._translations = {};
8
10
  this._defaultTranslations = {};
9
11
  this._currentLanguage = defaultLang;
12
+ this._languageListeners = [];
10
13
 
11
14
  this.loadTranslations = createMockMethod(async (lang) => {
12
15
  this._currentLanguage = lang;
13
16
  });
14
17
 
15
- this.translate = createMockMethod((key, params) => {
16
- const value = this._translations[key] || this._defaultTranslations[key] || key;
17
- if (!params) return value;
18
- return Object.entries(params).reduce(
19
- (acc, [k, v]) => acc.replace(`{${k}}`, v),
20
- value
21
- );
18
+ this.setCurrentLanguage = createMockMethod(async (lang) => {
19
+ await this.loadTranslations(lang);
20
+ this._languageListeners.forEach((callback) => callback({ lang }));
22
21
  });
23
22
 
24
- this.initTranslations = createMockMethod(async () => {
25
- const lang = this._LANGUAGES.includes('browser') ? 'browser' : this._DEFAULT_LANGUAGE;
26
- await this.loadTranslations(lang);
23
+ // Returns the unsubscribe function, like I18n.onLanguageChange.
24
+ this.onLanguageChange = createMockMethod((callback) => {
25
+ this._languageListeners.push(callback);
26
+ return () => {
27
+ this._languageListeners = this._languageListeners.filter((listener) => listener !== callback);
28
+ };
27
29
  });
28
30
 
29
- this.resetTranslations = createMockMethod(() => {
30
- this._translations = {};
31
- this._defaultTranslations = {};
32
- this._currentLanguage = this._DEFAULT_LANGUAGE;
31
+ // Same lookup as I18n.translate: nested dot keys, default-language fallback,
32
+ // then the given textContent, then the key itself.
33
+ this.translate = createMockMethod((key, textContent) => {
34
+ return this._lookup(key) || textContent || key;
33
35
  });
34
36
 
35
- this.getBrowserLanguage = createMockMethod(() => 'en');
37
+ this.getTranslationObject = createMockMethod((key) => this._lookup(key) || null);
36
38
 
37
- this.loadDefaultTranslations = createMockMethod(async () => {
38
- this._defaultTranslations = { 'fallback.key': 'Fallback Value' };
39
+ this.initTranslations = createMockMethod(async () => {
40
+ await this.loadTranslations(this._DEFAULT_LANGUAGE);
39
41
  });
40
42
 
41
- this.getNestedValue = createMockMethod((obj, keys) => {
42
- return keys.reduce((acc, key) => acc?.[key], obj);
43
+ this.resetTranslations = createMockMethod(() => {
44
+ this._translations = {};
45
+ this._defaultTranslations = {};
46
+ this._currentLanguage = this._DEFAULT_LANGUAGE;
43
47
  });
44
48
 
45
49
  this.onDestroy = createMockMethod(() => {
@@ -51,6 +55,26 @@ export class MockI18n {
51
55
  return this._currentLanguage;
52
56
  }
53
57
 
58
+ get defaultLanguage() {
59
+ return this._DEFAULT_LANGUAGE;
60
+ }
61
+
62
+ get languages() {
63
+ return this._LANGUAGES;
64
+ }
65
+
66
+ get localStorageKey() {
67
+ return 'nutin-fav-lang';
68
+ }
69
+
70
+ _lookup(key) {
71
+ const keys = key.split('.');
72
+ const read = (obj) => keys.reduce((acc, k) => acc?.[k], obj);
73
+ let value = read(this._translations);
74
+ if (!value && this._currentLanguage !== this._DEFAULT_LANGUAGE) value = read(this._defaultTranslations);
75
+ return value;
76
+ }
77
+
54
78
  setTranslations(translations) {
55
79
  this._translations = translations;
56
80
  }
@@ -61,16 +85,17 @@ export class MockI18n {
61
85
 
62
86
  reset() {
63
87
  this.loadTranslations.mockReset();
88
+ this.setCurrentLanguage.mockReset();
89
+ this.onLanguageChange.mockReset();
64
90
  this.translate.mockReset();
91
+ this.getTranslationObject.mockReset();
65
92
  this.initTranslations.mockReset();
66
93
  this.resetTranslations.mockReset();
67
- this.getBrowserLanguage.mockReset();
68
- this.loadDefaultTranslations.mockReset();
69
- this.getNestedValue.mockReset();
70
94
  this.onDestroy.mockReset();
71
95
 
72
96
  this._translations = {};
73
97
  this._defaultTranslations = {};
74
98
  this._currentLanguage = this._DEFAULT_LANGUAGE;
99
+ this._languageListeners = [];
75
100
  }
76
101
  }
@@ -1,57 +1,78 @@
1
1
  import { MockI18n } from './mock-i18n.js';
2
2
 
3
3
  describe('MockI18n', () => {
4
- it('exposes the constructor defaults via currentLanguage', () => {
4
+ it('exposes the constructor defaults via the language getters', () => {
5
5
  const i18n = new MockI18n('fr', ['en', 'fr']);
6
+
6
7
  expect(i18n.currentLanguage).toBe('fr');
8
+ expect(i18n.defaultLanguage).toBe('fr');
9
+ expect(i18n.languages).toEqual(['en', 'fr']);
7
10
  });
8
11
 
9
- it('translate/getBrowserLanguage run their built-in default implementation without needing .mockImplementation', () => {
12
+ it('translate() falls back to textContent, then to the key itself', () => {
10
13
  const i18n = new MockI18n();
11
14
 
12
- // No translations set — translate() falls back to returning the key itself.
15
+ expect(i18n.translate('some.key', 'Fallback text')).toBe('Fallback text');
13
16
  expect(i18n.translate('some.key')).toBe('some.key');
14
- expect(i18n.getBrowserLanguage()).toBe('en');
15
17
  });
16
18
 
17
- it('loadTranslations/initTranslations run their default implementation and update currentLanguage', async () => {
18
- const i18n = new MockI18n('en', ['en', 'fr']);
19
+ it('translate() resolves nested dot keys from setTranslations()', () => {
20
+ const i18n = new MockI18n();
21
+ i18n.setTranslations({ home: { title: 'Home' } });
19
22
 
23
+ expect(i18n.translate('home.title')).toBe('Home');
24
+ });
25
+
26
+ it('translate() falls back to the default-language translations off the default language', async () => {
27
+ const i18n = new MockI18n('en', ['en', 'fr']);
28
+ i18n.setDefaultTranslations({ greeting: 'Hello' });
20
29
  await i18n.loadTranslations('fr');
21
- expect(i18n.currentLanguage).toBe('fr');
30
+
31
+ expect(i18n.translate('greeting')).toBe('Hello');
22
32
  });
23
33
 
24
- it('translate is still trackable as a mock alongside its default implementation', () => {
34
+ it('getTranslationObject() returns the nested value, or null', () => {
25
35
  const i18n = new MockI18n();
26
- i18n.translate('some.key');
36
+ i18n.setTranslations({ nav: { home: 'Home', about: 'About' } });
27
37
 
28
- expect(i18n.translate).toHaveBeenCalledWith('some.key');
38
+ expect(i18n.getTranslationObject('nav')).toEqual({ home: 'Home', about: 'About' });
39
+ expect(i18n.getTranslationObject('missing')).toBe(null);
29
40
  });
30
41
 
31
- it('translate can be given real interpolation behavior via .mockImplementation', () => {
32
- const i18n = new MockI18n();
33
- i18n.setTranslations({ greeting: 'Hello {name}' });
34
- i18n.translate.mockImplementation((key, params) => {
35
- const value = i18n._translations[key] || key;
36
- if (!params) return value;
37
- return Object.entries(params).reduce((acc, [k, v]) => acc.replace(`{${k}}`, v), value);
38
- });
42
+ it('setCurrentLanguage() updates currentLanguage and notifies onLanguageChange() callbacks', async () => {
43
+ const i18n = new MockI18n('en', ['en', 'fr']);
44
+ let payload;
45
+ i18n.onLanguageChange((data) => { payload = data; });
39
46
 
40
- expect(i18n.translate('greeting', { name: 'Ada' })).toBe('Hello Ada');
47
+ await i18n.setCurrentLanguage('fr');
48
+
49
+ expect(i18n.currentLanguage).toBe('fr');
50
+ expect(payload).toEqual({ lang: 'fr' });
51
+ expect(i18n.setCurrentLanguage).toHaveBeenCalledWith('fr');
52
+ });
53
+
54
+ it('onLanguageChange() returns a function that stops further notifications', async () => {
55
+ const i18n = new MockI18n('en', ['en', 'fr']);
56
+ let calls = 0;
57
+ const unsubscribe = i18n.onLanguageChange(() => { calls++; });
58
+ unsubscribe();
59
+
60
+ await i18n.setCurrentLanguage('fr');
61
+
62
+ expect(calls).toBe(0);
41
63
  });
42
64
 
43
- it('setTranslations/setDefaultTranslations seed real, readable state', () => {
65
+ it('translate is still trackable as a mock alongside its default implementation', () => {
44
66
  const i18n = new MockI18n();
45
- i18n.setTranslations({ a: '1' });
46
- i18n.setDefaultTranslations({ b: '2' });
67
+ i18n.translate('some.key');
47
68
 
48
- expect(i18n._translations).toEqual({ a: '1' });
49
- expect(i18n._defaultTranslations).toEqual({ b: '2' });
69
+ expect(i18n.translate).toHaveBeenCalledWith('some.key');
50
70
  });
51
71
 
52
- it('reset() restores the constructor default language and clears translations', () => {
53
- const i18n = new MockI18n('en');
72
+ it('reset() restores the constructor default language and clears translations', async () => {
73
+ const i18n = new MockI18n('en', ['en', 'fr']);
54
74
  i18n.setTranslations({ a: '1' });
75
+ await i18n.setCurrentLanguage('fr');
55
76
 
56
77
  i18n.reset();
57
78
 
@@ -1,23 +1,31 @@
1
1
  import { createMockMethod } from './create-mock-method.js';
2
2
 
3
+ // Mirrors Router's public API. Route params are seeded with setParams() and read back
4
+ // through the real getCurrentParams()/getParam() shapes.
3
5
  export class MockRouter {
4
6
  constructor(routes = {}) {
5
7
  this.routes = routes;
6
- this._currentView = null;
8
+ this._currentParams = {};
7
9
 
8
- this.navigate = createMockMethod();
9
- this.handlePopState = createMockMethod();
10
- this.handleNotFound = createMockMethod();
11
- this.handleGuards = createMockMethod();
12
- this.initializeEventListeners = createMockMethod();
10
+ this.navigate = createMockMethod(async () => {});
11
+ this.reload = createMockMethod(async () => {});
12
+ this.getCurrentParams = createMockMethod(() => ({ ...this._currentParams }));
13
+ this.getParam = createMockMethod((key) => this._currentParams[key]);
14
+ this.removeEventListeners = createMockMethod();
15
+ this.onDestroy = createMockMethod();
16
+ }
17
+
18
+ setParams(params) {
19
+ this._currentParams = { ...params };
13
20
  }
14
21
 
15
22
  reset() {
16
23
  this.navigate.mockReset();
17
- this.handlePopState.mockReset();
18
- this.handleNotFound.mockReset();
19
- this.handleGuards.mockReset();
20
- this.initializeEventListeners.mockReset();
21
- this._currentView = null;
24
+ this.reload.mockReset();
25
+ this.getCurrentParams.mockReset();
26
+ this.getParam.mockReset();
27
+ this.removeEventListeners.mockReset();
28
+ this.onDestroy.mockReset();
29
+ this._currentParams = {};
22
30
  }
23
31
  }
@@ -8,22 +8,39 @@ describe('MockRouter', () => {
8
8
  expect(router.routes).toBe(routes);
9
9
  });
10
10
 
11
- it('navigate/handlePopState/handleNotFound/handleGuards/initializeEventListeners are independent bare mocks', () => {
11
+ it('navigate/reload are independent trackable mocks', async () => {
12
12
  const router = new MockRouter();
13
- router.navigate('/about');
13
+ await router.navigate('/about');
14
14
 
15
15
  expect(router.navigate.calls).toEqual([['/about']]);
16
- expect(router.handlePopState.calls).toEqual([]);
16
+ expect(router.reload.calls).toEqual([]);
17
17
  });
18
18
 
19
- it('reset() clears every mock method and _currentView', () => {
19
+ it('getCurrentParams/getParam read the params seeded with setParams()', () => {
20
20
  const router = new MockRouter();
21
- router.navigate('/about');
22
- router._currentView = 'about-view';
21
+ router.setParams({ id: '42' });
22
+
23
+ expect(router.getCurrentParams()).toEqual({ id: '42' });
24
+ expect(router.getParam('id')).toBe('42');
25
+ expect(router.getParam('missing')).toBe(undefined);
26
+ });
27
+
28
+ it('getCurrentParams returns a copy', () => {
29
+ const router = new MockRouter();
30
+ router.setParams({ id: '42' });
31
+ router.getCurrentParams().id = 'changed';
32
+
33
+ expect(router.getParam('id')).toBe('42');
34
+ });
35
+
36
+ it('reset() clears every mock method and the seeded params', async () => {
37
+ const router = new MockRouter();
38
+ await router.navigate('/about');
39
+ router.setParams({ id: '42' });
23
40
 
24
41
  router.reset();
25
42
 
26
43
  expect(router.navigate.calls).toEqual([]);
27
- expect(router._currentView).toBe(null);
44
+ expect(router.getCurrentParams()).toEqual({});
28
45
  });
29
46
  });
@@ -12,7 +12,9 @@ const verboseEnabled = args.includes('--verbose');
12
12
  const fileFilters = args.filter(arg => arg !== '--coverage' && arg !== '--verbose');
13
13
 
14
14
  // Tests import and run the compiled build output (dist/src/)
15
+ {{#unless (eq lang "js")}}
15
16
  // and has no sourcemap back to the .ts sources
17
+ {{/unless}}
16
18
  const origins = [];
17
19
  const coverageDirs = [];
18
20
 
@@ -41,10 +43,8 @@ if (fileFilters.length) {
41
43
  }
42
44
 
43
45
  async function loadTestFile(file) {
44
- setupJsdom();
45
46
  setCurrentTestFile(file);
46
47
  await import(file);
47
- teardownJsdom();
48
48
  }
49
49
 
50
50
  async function runCoverage(testResults) {
@@ -77,18 +77,27 @@ async function runTests() {
77
77
 
78
78
  if (coverageEnabled) await startCoverage();
79
79
 
80
+ // One DOM for importing every file (module-level code may use it); each suite
81
+ // then runs in a fresh one (see runQueuedTests).
82
+ let loadFailures = 0;
83
+ setupJsdom();
80
84
  for (const file of testFiles) {
81
85
  try {
82
86
  await loadTestFile(file);
83
87
  } catch (error) {
88
+ loadFailures++;
84
89
  print.boldError(`✗ ${file} failed to load:`);
85
90
  print.grayError(error.stack);
86
91
  }
87
92
  }
93
+ teardownJsdom();
88
94
 
89
95
  const testResults = await runQueuedTests();
90
96
 
91
- if (testResults.failed > 0) process.exitCode = 1;
97
+ if (loadFailures > 0) {
98
+ print.boldError(`✗ ${loadFailures} test file(s) failed to load — see errors above.\n`);
99
+ }
100
+ if (testResults.failed > 0 || loadFailures > 0) process.exitCode = 1;
92
101
 
93
102
  if (coverageEnabled) await runCoverage(testResults);
94
103
  }