@nutin/cli 2.1.0 → 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 (241) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/README.md +14 -6
  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 +12 -11
  34. package/templates/base/.gitignore.hbs +1 -0
  35. package/templates/base/AGENTS.md.hbs +45 -43
  36. package/templates/base/GETTING_STARTED.md.hbs +32 -23
  37. package/templates/base/README.md.hbs +7 -4
  38. package/templates/base/nutin.config.js.hbs +1 -1
  39. package/templates/base/src/app/components/.gitkeep +0 -0
  40. package/templates/base/src/app/globals.d.ts.hbs +6 -3
  41. package/templates/base/src/app/guards.ts.hbs +1 -3
  42. package/templates/base/src/app/main.ts.hbs +2 -9
  43. package/templates/base/src/app/routes.ts.hbs +9 -3
  44. package/templates/base/src/app/services/.gitkeep +0 -0
  45. package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
  46. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  47. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
  48. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
  49. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
  50. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
  51. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
  52. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +307 -21
  53. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
  54. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +105 -4
  55. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +136 -62
  56. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
  57. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +101 -51
  58. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
  59. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
  60. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
  61. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
  62. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
  63. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
  64. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
  65. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
  66. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
  67. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
  68. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
  69. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
  70. package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
  71. package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
  72. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
  73. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
  74. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
  75. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
  76. package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
  77. package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
  78. package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
  79. package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
  80. package/templates/base/src/core/index.ts.hbs +4 -2
  81. package/templates/base/src/core/internals.d.ts.hbs +199 -20
  82. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
  83. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
  84. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
  85. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
  86. package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
  87. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
  88. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
  89. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
  90. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
  91. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
  92. package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
  93. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
  94. package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
  95. package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
  96. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
  97. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
  98. package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +217 -4
  100. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +210 -65
  101. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
  102. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
  103. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
  104. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
  105. package/templates/base/src/core/services/router/router.test.js.hbs +321 -0
  106. package/templates/base/src/core/services/router/router.ts.hbs +122 -54
  107. package/templates/base/src/index.html.hbs +1 -0
  108. package/templates/base/src/styles/_styles.scss.hbs +1 -113
  109. package/templates/base/src/styles/main.scss.hbs +1 -1
  110. package/templates/base/tools/builder/builder.config.js.hbs +6 -0
  111. package/templates/base/tools/builder/builder.js.hbs +26 -8
  112. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
  113. package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
  114. package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
  115. package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
  116. package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
  117. package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
  118. package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
  119. package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
  120. package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
  121. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
  122. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
  123. package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
  124. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
  125. package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
  126. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
  127. package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
  128. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
  129. package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
  130. package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
  131. package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
  132. package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
  133. package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
  134. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
  135. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +25 -8
  136. package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
  137. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
  138. package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
  139. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
  140. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +61 -15
  141. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
  142. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
  143. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +48 -18
  144. package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
  145. package/templates/base/tools/builder/core/seo/route-output.js.hbs +26 -8
  146. package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
  147. package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
  148. package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
  149. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
  150. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
  151. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
  152. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +26 -9
  153. package/templates/base/tools/builder/core/styles/sass.js.hbs +7 -5
  154. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
  155. package/templates/base/tools/dev/dev-serve.js.hbs +71 -7
  156. package/templates/base/tools/dev/serve.js.hbs +152 -22
  157. package/templates/base/tools/dev/watcher.js.hbs +75 -28
  158. package/templates/base/tools/generator/generator.js.hbs +23 -18
  159. package/templates/base/tools/generator/handle-file.js.hbs +4 -24
  160. package/templates/base/tools/generator/handle-file.test.js.hbs +4 -23
  161. package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
  162. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
  163. package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
  164. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
  165. package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
  166. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  167. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
  168. package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
  169. package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
  170. package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
  171. package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
  172. package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
  173. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
  174. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
  175. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
  176. package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
  177. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
  178. package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
  179. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
  180. package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
  181. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
  182. package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
  183. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
  184. package/templates/base/tools/utils/build-lock.js.hbs +97 -0
  185. package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
  186. package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
  187. package/templates/base/tools/utils/format.js.hbs +1 -1
  188. package/templates/base/tools/utils/index.js.hbs +3 -1
  189. package/templates/base/tools/utils/print.js.hbs +2 -2
  190. package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
  191. package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
  192. package/templates/base/tools/utils/run-command.js.hbs +30 -1
  193. package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
  194. package/templates/base/tools/utils/run-script.js.hbs +2 -2
  195. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +5 -2
  196. package/templates/features/docker/tools/docker/nginx.conf.template +19 -2
  197. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
  198. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
  199. package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
  200. package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
  201. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
  202. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
  203. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
  204. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
  205. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +29 -0
  206. package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
  207. package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
  208. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
  209. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
  210. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
  211. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
  212. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
  213. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
  214. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
  215. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
  216. package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
  217. package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
  218. package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
  219. package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
  220. package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
  221. package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
  222. package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
  223. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
  224. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
  225. package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
  226. package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
  227. package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
  228. package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
  229. package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
  230. package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
  231. package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
  232. package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
  233. package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
  234. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
  235. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
  236. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
  237. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
  238. package/templates/base/src/app/components/index.ts.hbs +0 -1
  239. package/templates/base/src/app/services/index.ts.hbs +0 -1
  240. package/templates/base/src/app/views/index.ts.hbs +0 -3
  241. /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
@@ -1,4 +1,4 @@
1
- import { BaseComponent, AppEventBus } from '#root/dist/src/core/index.js';
1
+ import { BaseComponent, AppEventBus, AppPipeRegistry } from '#root/dist/src/core/index.js';
2
2
 
3
3
  class TestComponent extends BaseComponent {
4
4
  constructor(options = {}, hooks = {}) {
@@ -37,6 +37,87 @@ class TestComponent extends BaseComponent {
37
37
  }
38
38
  }
39
39
 
40
+ class InputChild extends BaseComponent {
41
+ constructor(options) {
42
+ super(options);
43
+ this.renders = 0;
44
+ this.keptCount = 0;
45
+ }
46
+ generateTemplate() { return '<input>'; }
47
+ onBeforeRender() { this.renders++; }
48
+ onKept() { this.keptCount++; }
49
+ }
50
+
51
+ class KeepingParent extends BaseComponent {
52
+ constructor(options = {}, childTemplate = '<div data-component="child"></div>') {
53
+ super(options);
54
+ this.childTemplate = childTemplate;
55
+ this.keep = 'a';
56
+ this.created = [];
57
+ }
58
+ generateTemplate() { return `<h1>parent</h1>${this.childTemplate}`; }
59
+ registerChildren() {
60
+ return [{
61
+ selector: 'child',
62
+ keep: this.keep,
63
+ factory: (el) => {
64
+ const child = new InputChild({ mountTarget: el });
65
+ this.created.push(child);
66
+ return child;
67
+ },
68
+ }];
69
+ }
70
+ }
71
+
72
+ class LoggingChild extends BaseComponent {
73
+ constructor(options, log, name, hooks = {}) {
74
+ super(options);
75
+ this.log = log;
76
+ this.name = name;
77
+ this.hooks = hooks;
78
+ this.keptCount = 0;
79
+ }
80
+ generateTemplate() { return this.hooks.template ?? `<input class="${this.name}">`; }
81
+ registerChildren() { return this.hooks.children?.(this) ?? []; }
82
+ onBeforeRender() { this.log.push(`${this.name}:render`); }
83
+ onKept() {
84
+ this.keptCount++;
85
+ this.log.push(`${this.name}:kept`);
86
+ this.hooks.onKept?.(this);
87
+ }
88
+ }
89
+
90
+ class LoggingParent extends BaseComponent {
91
+ constructor(log, hooks = {}) {
92
+ super({});
93
+ this.log = log;
94
+ this.hooks = hooks;
95
+ this.children = [];
96
+ }
97
+ generateTemplate() { return this.hooks.template ?? '<div data-component="a"></div><div data-component="b"></div>'; }
98
+ registerChildren() {
99
+ return this.hooks.children?.(this) ?? ['a', 'b'].map(name => ({
100
+ selector: name,
101
+ keep: name,
102
+ factory: (el) => {
103
+ const child = new LoggingChild({ mountTarget: el }, this.log, name, this.hooks.childHooks?.[name]);
104
+ this.children.push(child);
105
+ return child;
106
+ },
107
+ }));
108
+ }
109
+ compose() {
110
+ this.log.push('parent:compose');
111
+ if (this.hooks.compose) this.hooks.compose(this);
112
+ else super.compose();
113
+ }
114
+ hydrate() {
115
+ this.log.push('parent:hydrate');
116
+ super.hydrate();
117
+ }
118
+ onAfterRender() { this.log.push('parent:afterRender'); }
119
+ }
120
+
40
121
  describe('BaseComponent', () => {
41
122
  let app;
42
123
 
@@ -155,4 +236,383 @@ describe('BaseComponent', () => {
155
236
  expect(configs.length).toBe(1);
156
237
  component.destroy();
157
238
  });
239
+
240
+ it('render() keeps a child with a keep value, its typed value and focus across a parent re-render, and calls onKept() on it', () => {
241
+ const parent = new KeepingParent();
242
+ parent.render();
243
+ const [child] = parent.created;
244
+ const input = child.getElement().querySelector('input');
245
+ input.value = 'typed';
246
+ input.focus();
247
+
248
+ parent.render();
249
+
250
+ expect(parent.created.length).toBe(1);
251
+ expect(child.renders).toBe(1);
252
+ expect(child.keptCount).toBe(1);
253
+ expect(parent.getElement().contains(input)).toBe(true);
254
+ expect(input.value).toBe('typed');
255
+ expect(document.activeElement).toBe(input);
256
+ parent.destroy();
257
+ });
258
+
259
+ it('render() recreates a child with a keep value whose key changed and does not call onKept() on the new one', () => {
260
+ const parent = new KeepingParent();
261
+ parent.render();
262
+ parent.keep = 'b';
263
+
264
+ parent.render();
265
+
266
+ expect(parent.created.length).toBe(2);
267
+ expect(parent.created[0].keptCount).toBe(0);
268
+ expect(parent.created[1].keptCount).toBe(0);
269
+ expect(parent.getElement().contains(parent.created[0].getElement())).toBe(false);
270
+ parent.destroy();
271
+ });
272
+
273
+ it('destroy() destroys kept children too', () => {
274
+ const parent = new KeepingParent();
275
+ parent.render();
276
+ parent.render();
277
+ const childEl = parent.created[0].getElement();
278
+
279
+ parent.destroy();
280
+
281
+ expect(childEl.isConnected).toBe(false);
282
+ });
283
+
284
+ it('render() keeps keepBy catalog items that did not change and recreates the ones that did', () => {
285
+ class Item extends BaseComponent {
286
+ constructor(el, data) {
287
+ super({ mountTarget: el });
288
+ this.data = data;
289
+ }
290
+ generateTemplate() { return `<span>${this.data.name}</span>`; }
291
+ }
292
+ class List extends BaseComponent {
293
+ constructor() {
294
+ super({});
295
+ this.items = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }];
296
+ }
297
+ generateTemplate() { return '<ul data-catalog="list"></ul>'; }
298
+ registerChildren() {
299
+ return this.createCatalogComponents({
300
+ items: this.items,
301
+ selector: 'list',
302
+ elementName: 'item',
303
+ elementTag: 'li',
304
+ component: Item,
305
+ keepBy: (item) => item.id,
306
+ });
307
+ }
308
+ }
309
+ const list = new List();
310
+ list.render();
311
+ const [first, second] = Array.from(list.getElement().querySelectorAll('span'));
312
+
313
+ list.items = [{ id: 0, name: 'new' }, { id: 1, name: 'a' }, { id: 2, name: 'changed' }];
314
+ list.render();
315
+
316
+ const spans = Array.from(list.getElement().querySelectorAll('span'));
317
+ expect(spans.map(span => span.textContent)).toEqual(['new', 'a', 'changed']);
318
+ expect(spans[1]).toBe(first);
319
+ expect(spans[2]).not.toBe(second);
320
+ expect(Array.from(list.getElement().querySelectorAll('li')).map(li => li.dataset.index)).toEqual(['0', '1', '2']);
321
+ list.destroy();
322
+ });
323
+
324
+ it('render() does not pipe a kept child\'s content again', () => {
325
+ silenceConsole('warn', () => AppPipeRegistry.register('baseComponentExclaim', (value) => `${value}!`));
326
+ class PipedChild extends BaseComponent {
327
+ generateTemplate() { return '<span data-pipe="baseComponentExclaim">hi</span>'; }
328
+ }
329
+ class Parent extends BaseComponent {
330
+ generateTemplate() { return '<div data-component="child"></div>'; }
331
+ registerChildren() {
332
+ return [{ selector: 'child', keep: 'c', factory: (el) => new PipedChild({ mountTarget: el }) }];
333
+ }
334
+ }
335
+ const parent = new Parent({});
336
+
337
+ parent.render();
338
+ parent.render();
339
+
340
+ expect(parent.getElement().querySelector('span').textContent).toBe('hi!');
341
+ parent.destroy();
342
+ });
343
+
344
+ it('render() parks kept children with moveBefore and leaves no parking element behind', () => {
345
+ const moved = [];
346
+ Element.prototype.moveBefore = function (node, child) {
347
+ moved.push(node);
348
+ this.insertBefore(node, child);
349
+ };
350
+
351
+ try {
352
+ const parent = new KeepingParent();
353
+ parent.render();
354
+ const childEl = parent.created[0].getElement();
355
+ const bodyChildren = document.body.children.length;
356
+
357
+ parent.render();
358
+
359
+ expect(moved).toEqual([childEl, childEl]);
360
+ expect(parent.getElement().contains(childEl)).toBe(true);
361
+ expect(document.body.children.length).toBe(bodyChildren);
362
+ parent.destroy();
363
+ } finally {
364
+ delete Element.prototype.moveBefore;
365
+ }
366
+ });
367
+
368
+ it('onKept() runs once per parent re-render, for every kept child', () => {
369
+ const parent = new LoggingParent([]);
370
+ parent.render();
371
+ parent.render();
372
+ parent.render();
373
+
374
+ expect(parent.children.length).toBe(2);
375
+ expect(parent.children.map(child => child.keptCount)).toEqual([2, 2]);
376
+ parent.destroy();
377
+ });
378
+
379
+ it('onKept() runs after compose() with the child back in place, before the parent hydrates and runs onAfterRender()', () => {
380
+ const log = [];
381
+ let connectedWhenKept;
382
+ const parent = new LoggingParent(log, {
383
+ childHooks: { a: { onKept: (child) => { connectedWhenKept = parent.getElement().contains(child.getElement()) && child.getElement().isConnected; } } },
384
+ });
385
+ parent.render();
386
+ log.length = 0;
387
+
388
+ parent.render();
389
+
390
+ expect(log).toEqual(['parent:compose', 'a:kept', 'b:kept', 'parent:hydrate', 'parent:afterRender']);
391
+ expect(connectedWhenKept).toBe(true);
392
+ parent.destroy();
393
+ });
394
+
395
+ it('onKept() is not called on a child created by the same render, even when a sibling is kept', () => {
396
+ const log = [];
397
+ let bKeep = 'b';
398
+ const parent = new LoggingParent(log, {
399
+ children: (self) => [
400
+ { selector: 'a', keep: 'a', factory: (el) => { const c = new LoggingChild({ mountTarget: el }, log, 'a'); self.children.push(c); return c; } },
401
+ { selector: 'b', keep: bKeep, factory: (el) => { const c = new LoggingChild({ mountTarget: el }, log, 'b'); self.children.push(c); return c; } },
402
+ ],
403
+ });
404
+ parent.render();
405
+ bKeep = 'b2';
406
+ log.length = 0;
407
+
408
+ parent.render();
409
+
410
+ expect(log.filter(entry => entry.endsWith(':kept'))).toEqual(['a:kept']);
411
+ expect(log.filter(entry => entry.endsWith(':render'))).toEqual(['b:render']);
412
+ parent.destroy();
413
+ });
414
+
415
+ it('onKept() is called on unchanged keepBy catalog items only', () => {
416
+ const kept = [];
417
+ class Item extends BaseComponent {
418
+ constructor(el, data) {
419
+ super({ mountTarget: el });
420
+ this.data = data;
421
+ }
422
+ generateTemplate() { return `<span>${this.data.name}</span>`; }
423
+ onKept() { kept.push(this.data.name); }
424
+ }
425
+ class List extends BaseComponent {
426
+ constructor() {
427
+ super({});
428
+ this.items = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'c' }];
429
+ }
430
+ generateTemplate() { return '<ul data-catalog="list"></ul>'; }
431
+ registerChildren() {
432
+ return this.createCatalogComponents({
433
+ items: this.items, selector: 'list', elementName: 'item', component: Item, keepBy: (item) => item.id,
434
+ });
435
+ }
436
+ }
437
+ const list = new List();
438
+ list.render();
439
+ list.items = [{ id: 4, name: 'new' }, { id: 1, name: 'a' }, { id: 2, name: 'changed' }];
440
+
441
+ list.render();
442
+
443
+ expect(kept).toEqual(['a']);
444
+ list.destroy();
445
+ });
446
+
447
+ it('onKept() is not called on children of a kept child, which are left untouched', () => {
448
+ const log = [];
449
+ const parent = new LoggingParent(log, {
450
+ template: '<div data-component="a"></div>',
451
+ children: (self) => [{
452
+ selector: 'a',
453
+ keep: 'a',
454
+ factory: (el) => {
455
+ const child = new LoggingChild({ mountTarget: el }, log, 'a', {
456
+ template: '<div data-component="inner"></div>',
457
+ children: () => [{ selector: 'inner', keep: 'inner', factory: (innerEl) => new LoggingChild({ mountTarget: innerEl }, log, 'inner') }],
458
+ });
459
+ self.children.push(child);
460
+ return child;
461
+ },
462
+ }],
463
+ });
464
+ parent.render();
465
+ log.length = 0;
466
+
467
+ parent.render();
468
+
469
+ expect(log).toEqual(['parent:compose', 'a:kept', 'parent:hydrate', 'parent:afterRender']);
470
+ parent.destroy();
471
+ });
472
+
473
+ it('onKept() is not called when compose() is overridden without calling super, and is not replayed later', () => {
474
+ const log = [];
475
+ let skipCompose = false;
476
+ const parent = new LoggingParent(log, {
477
+ compose: (self) => { if (!skipCompose) BaseComponent.prototype.compose.call(self); },
478
+ });
479
+ parent.render();
480
+ parent.render();
481
+ expect(parent.children.map(child => child.keptCount)).toEqual([1, 1]);
482
+
483
+ skipCompose = true;
484
+ parent.render();
485
+ expect(parent.children.map(child => child.keptCount)).toEqual([1, 1]);
486
+
487
+ // compose() runs again: the still-registered children are kept, not recreated.
488
+ skipCompose = false;
489
+ parent.render();
490
+ expect(parent.children.length).toBe(2);
491
+ expect(parent.children.map(child => child.keptCount)).toEqual([2, 2]);
492
+ parent.destroy();
493
+ });
494
+
495
+ it('onKept() is skipped when compose() throws, and the next render keptCount normally', () => {
496
+ const log = [];
497
+ let fail = false;
498
+ const parent = new LoggingParent(log, {
499
+ compose: (self) => {
500
+ BaseComponent.prototype.compose.call(self);
501
+ if (fail) throw new Error('compose failed');
502
+ },
503
+ });
504
+ parent.render();
505
+
506
+ fail = true;
507
+ expect(() => parent.render()).toThrow('compose failed');
508
+ expect(parent.children.map(child => child.keptCount)).toEqual([0, 0]);
509
+
510
+ fail = false;
511
+ parent.render();
512
+ expect(parent.children.length).toBe(2);
513
+ expect(parent.children.map(child => child.keptCount)).toEqual([1, 1]);
514
+ expect(parent.getElement().contains(parent.children[0].getElement())).toBe(true);
515
+ parent.destroy();
516
+ });
517
+
518
+ it('onKept() can re-render the kept child itself, which stays in place', () => {
519
+ const log = [];
520
+ let text = 'first';
521
+ const parent = new LoggingParent(log, {
522
+ template: '<div data-component="a"></div>',
523
+ children: (self) => [{
524
+ selector: 'a',
525
+ keep: 'a',
526
+ factory: (el) => {
527
+ const child = new LoggingChild({ mountTarget: el }, log, 'a', {
528
+ onKept: (c) => c.render(),
529
+ });
530
+ child.generateTemplate = () => `<p>${text}</p>`;
531
+ self.children.push(child);
532
+ return child;
533
+ },
534
+ }],
535
+ });
536
+ parent.render();
537
+ text = 'second';
538
+
539
+ parent.render();
540
+
541
+ const [child] = parent.children;
542
+ expect(parent.children.length).toBe(1);
543
+ expect(parent.getElement().contains(child.getElement())).toBe(true);
544
+ expect(child.getElement().innerHTML).toBe('<p>second</p>');
545
+ parent.destroy();
546
+ });
547
+
548
+ it('onKept() re-rendering the parent is a no-op thanks to the re-entrancy guard', () => {
549
+ const log = [];
550
+ const parent = new LoggingParent(log, {
551
+ template: '<div data-component="a"></div>',
552
+ children: (self) => [{
553
+ selector: 'a',
554
+ keep: 'a',
555
+ factory: (el) => {
556
+ const child = new LoggingChild({ mountTarget: el }, log, 'a', { onKept: () => parent.render() });
557
+ self.children.push(child);
558
+ return child;
559
+ },
560
+ }],
561
+ });
562
+ parent.render();
563
+ log.length = 0;
564
+
565
+ parent.render();
566
+
567
+ expect(log.filter(entry => entry === 'parent:compose').length).toBe(1);
568
+ expect(parent.children.length).toBe(1);
569
+ parent.destroy();
570
+ });
571
+
572
+ it('onKept() sees restored focus on the detach-and-reinsert fallback path', () => {
573
+ let focusedWhenKept;
574
+ const parent = new LoggingParent([], {
575
+ template: '<div data-component="a"></div>',
576
+ children: (self) => [{
577
+ selector: 'a',
578
+ keep: 'a',
579
+ factory: (el) => {
580
+ const child = new LoggingChild({ mountTarget: el }, [], 'a', {
581
+ onKept: (c) => { focusedWhenKept = document.activeElement === c.getElement().querySelector('input'); },
582
+ });
583
+ self.children.push(child);
584
+ return child;
585
+ },
586
+ }],
587
+ });
588
+ parent.render();
589
+ parent.children[0].getElement().querySelector('input').focus();
590
+
591
+ parent.render();
592
+
593
+ expect(focusedWhenKept).toBe(true);
594
+ parent.destroy();
595
+ });
596
+
597
+ it('onKept() runs on the moveBefore path too, with the child already moved in place', () => {
598
+ Element.prototype.moveBefore = function (node, child) {
599
+ this.insertBefore(node, child);
600
+ };
601
+
602
+ try {
603
+ let inPlaceWhenKept;
604
+ const parent = new LoggingParent([], {
605
+ childHooks: { a: { onKept: (child) => { inPlaceWhenKept = parent.getElement().contains(child.getElement()); } } },
606
+ });
607
+ parent.render();
608
+
609
+ parent.render();
610
+
611
+ expect(parent.children.map(child => child.keptCount)).toEqual([1, 1]);
612
+ expect(inPlaceWhenKept).toBe(true);
613
+ parent.destroy();
614
+ } finally {
615
+ delete Element.prototype.moveBefore;
616
+ }
617
+ });
158
618
  });
@@ -1,50 +1,34 @@
1
- import { DomHelper } from './helpers/dom.helper.js';
2
- import { EventHelper } from './helpers/event.helper.js';
3
- import { I18nHelper } from './helpers/i18n.helper.js';
4
- import { PipeHelper } from './helpers/pipe.helper.js';
5
- import { ChildrenHelper } from './helpers/children.helper.js';
6
- import { CatalogHelper, CatalogConfig } from './helpers/catalog.helper.js';
7
- import { Component, AppEventBus } from '../../index.js';
8
- import { SecurityHelper, TrustLevel } from './helpers/security.helper.js';
9
-
10
- export { SecurityHelper, TrustLevel } from './helpers/security.helper.js';
11
- export { CatalogItemConfig, CatalogConfig, CatalogItemPrimitive } from './helpers/catalog.helper.js';
12
- export { TokenHelper } from './helpers/token.helper.js';
13
-
14
- /**```typescript
15
- * export interface ComponentConfig {
16
- * selector: string;
17
- * factory: (element: HTMLElement) => Component;
18
- * }
19
- * ```
20
- */
21
- export interface ComponentConfig {
22
- selector: string;
23
- factory: (element: HTMLElement) => Component;
24
- }
25
-
26
- export interface BaseComponentOptions {
27
- template?: string;
28
- mountTarget?: string | HTMLElement;
29
- tagName?: keyof HTMLElementTagNameMap;
30
- trustLevel?: TrustLevel;
31
- }
1
+ import * as DomHelper from './helpers/dom.helper.js';
2
+ import * as EventHelper from './helpers/event.helper.js';
3
+ import * as I18nHelper from './helpers/i18n.helper.js';
4
+ import * as PipeHelper from './helpers/pipe.helper.js';
5
+ import * as ChildrenHelper from './helpers/children.helper.js';
6
+ import type { ChildRegistry } from './helpers/children.helper.js';
7
+ import * as PreservationHelper from './helpers/preservation.helper.js';
8
+ import * as CatalogHelper from './helpers/catalog.helper.js';
9
+ import { AppEventBus } from '../../services/event-bus/event-bus.js';
10
+ import * as SecurityHelper from './helpers/security.helper.js';
11
+
12
+ export * as SecurityHelper from './helpers/security.helper.js';
13
+ export * as TokenHelper from './helpers/token.helper.js';
14
+ export { html, raw, trustedRaw, SafeHtml } from './helpers/html.helper.js';
32
15
 
33
16
  export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
34
- private _children: BaseComponent[] = [];
17
+ private _children: ChildRegistry = ChildrenHelper.createRegistry();
35
18
  private _isRendering = false;
36
19
  protected element: T;
37
20
  protected eventListeners: Array<[EventTarget, string, EventListener]> = [];
38
- private _busSubscriptions: Array<[EventKey, (data: any) => void]> = [];
39
- protected trustLevel: TrustLevel | undefined;
21
+ private _busSubscriptions: Array<[Nutin.EventKey, (data: any) => void]> = [];
22
+ protected trustLevel: Nutin.TrustLevel | undefined;
40
23
 
41
24
  constructor({
42
25
  mountTarget = '#app',
43
26
  tagName = 'div',
44
27
  trustLevel
45
- }: BaseComponentOptions) {
28
+ }: Nutin.BaseComponentOptions) {
46
29
  this.trustLevel = trustLevel;
47
30
  this.element = DomHelper.createElement<T>(tagName, '', trustLevel);
31
+ DomHelper.markComponentRoot(this.element);
48
32
  DomHelper.mountElement(this.element, mountTarget);
49
33
  }
50
34
 
@@ -52,7 +36,7 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
52
36
  return this.element;
53
37
  }
54
38
 
55
- public registerChildren(): ComponentConfig[] {
39
+ public registerChildren(): Nutin.ComponentConfig[] {
56
40
  return [];
57
41
  }
58
42
 
@@ -62,11 +46,16 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
62
46
 
63
47
  try {
64
48
  this.onBeforeRender();
65
- this.element.innerHTML = SecurityHelper.sanitizeTemplate(
66
- this.generateTemplate(),
67
- this.trustLevel
68
- );
69
- this.compose();
49
+ const preserved = PreservationHelper.preserve(ChildrenHelper.getKeepableElements(this._children));
50
+ try {
51
+ this.element.replaceChildren(
52
+ SecurityHelper.sanitizeToFragment(this.generateTemplate(), this.trustLevel)
53
+ );
54
+ this.compose();
55
+ } finally {
56
+ PreservationHelper.restore(preserved);
57
+ }
58
+ this._children.kept.splice(0).forEach(child => child.onKept());
70
59
  this.hydrate();
71
60
  this.autoBindEvents()
72
61
  this.onAfterRender();
@@ -93,14 +82,16 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
93
82
  protected onAfterRender(): void {}
94
83
  protected onBeforeDestroy(): void {}
95
84
  protected onAfterDestroy(): void {}
85
+ // Called instead of a render when a parent re-render keeps this child as-is
86
+ protected onKept(): void {}
96
87
 
97
- protected generateTemplate(): string {
88
+ protected generateTemplate(): Nutin.Template {
98
89
  return '';
99
90
  }
100
91
 
101
92
  protected hydrate(): void {
102
93
  this.parseDataAttributes();
103
- DomHelper.cleanupOptionalContent();
94
+ DomHelper.cleanupOptionalContent(this.element);
104
95
  }
105
96
 
106
97
  protected compose(): void {
@@ -122,20 +113,20 @@ export abstract class BaseComponent<T extends HTMLElement = HTMLElement> {
122
113
  ChildrenHelper.addChildren(this, this.element, this._children);
123
114
  }
124
115
 
125
- protected createCatalogComponents(config: CatalogConfig): ComponentConfig[] {
116
+ protected createCatalogComponents(config: Nutin.CatalogConfig): Nutin.ComponentConfig[] {
126
117
  return CatalogHelper.generateCatalog(config, this.element);
127
118
  }
128
119
 
129
- protected listen<K extends EventKey>(
120
+ protected listen<K extends Nutin.EventKey>(
130
121
  event: K,
131
- callback: (data: EventMap[K]) => void
122
+ callback: (data: Nutin.EventMap[K]) => void
132
123
  ): void {
133
124
  AppEventBus.subscribe(event, callback);
134
125
  this._busSubscriptions.push([event, callback]);
135
126
  }
136
127
 
137
- protected listenToRenderEvents(events: EventKey[]): void {
138
- events.forEach((event: EventKey) => {
128
+ protected listenToRenderEvents(events: Nutin.EventKey[]): void {
129
+ events.forEach((event: Nutin.EventKey) => {
139
130
  this.listen(event, () => this.render());
140
131
  });
141
132
  }
@@ -1,4 +1,4 @@
1
- import { CatalogHelper } from '#root/dist/src/core/base-classes/base-component/helpers/catalog.helper.js';
1
+ import * as CatalogHelper from '#root/dist/src/core/base-classes/base-component/helpers/catalog.helper.js';
2
2
 
3
3
  describe('CatalogHelper', () => {
4
4
  let scope;
@@ -75,6 +75,7 @@ describe('CatalogHelper', () => {
75
75
  component: FakeComponent,
76
76
  props: { a: 1 },
77
77
  defaults: { b: 2 },
78
+ normalizeKeys: ['title'],
78
79
  },
79
80
  scope
80
81
  );
@@ -121,4 +122,38 @@ describe('CatalogHelper', () => {
121
122
  expect(calls[0]).toEqual({ value: 'red', index: 0 });
122
123
  expect(calls[1]).toEqual({ value: 'blue', index: 1 });
123
124
  });
125
+
126
+ it('without keepBy, configs carry no keep value so every item is recreated on re-render', () => {
127
+ scope.innerHTML = '<div data-catalog="list"></div>';
128
+ const configs = CatalogHelper.generateCatalog(
129
+ { items: [{ id: 1 }], selector: 'list', elementName: 'item', component: class {} },
130
+ scope
131
+ );
132
+ expect(configs[0].keep).toBe(undefined);
133
+ expect(configs[0].keepIf).toBe(undefined);
134
+ });
135
+
136
+ it('with keepBy, each config carries the keepBy value, a catalog-scoped identity and the raw item to compare', () => {
137
+ scope.innerHTML = '<div data-catalog="list"></div>';
138
+ const items = [{ id: 'a' }, { id: 'b' }];
139
+ class FakeComponent {}
140
+ const configs = CatalogHelper.generateCatalog(
141
+ {
142
+ items,
143
+ selector: 'list',
144
+ elementName: 'item',
145
+ component: FakeComponent,
146
+ props: { a: 1 },
147
+ keepBy: (item, index) => `${item.id}-${index}`,
148
+ },
149
+ scope
150
+ );
151
+
152
+ expect(configs[1].selector).toBe('item-1');
153
+ expect(configs[1].keep).toBe('b-1');
154
+ expect(configs[1].scope).toBe('catalog:list:item');
155
+ expect(configs[1].keepIf.item).toBe(items[1]);
156
+ expect(configs[1].keepIf.options).toEqual({ a: 1 });
157
+ expect(configs[1].keepIf.component).toBe(FakeComponent);
158
+ });
124
159
  });