@tinya-code/gas-admin-ui 0.2.0-alpha.15 → 0.2.0-alpha.17

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 (368) hide show
  1. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +84 -0
  2. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +1 -0
  3. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +140 -0
  4. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +1 -0
  5. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +113 -0
  6. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +1 -0
  7. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +163 -0
  8. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +1 -0
  9. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +1027 -0
  10. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +1 -0
  11. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +278 -0
  12. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +1 -0
  13. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +107 -0
  14. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +1 -0
  15. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +101 -0
  16. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +1 -0
  17. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +141 -0
  18. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +1 -0
  19. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +88 -0
  20. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +1 -0
  21. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +107 -0
  22. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +1 -0
  23. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +140 -0
  24. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +1 -0
  25. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +119 -0
  26. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +1 -0
  27. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +125 -0
  28. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +1 -0
  29. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +152 -0
  30. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +1 -0
  31. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +211 -0
  32. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +1 -0
  33. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +116 -0
  34. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +1 -0
  35. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +111 -0
  36. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +1 -0
  37. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +122 -0
  38. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +1 -0
  39. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +107 -0
  40. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +1 -0
  41. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +109 -0
  42. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +1 -0
  43. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +45 -0
  44. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +1 -0
  45. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +50 -0
  46. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +1 -0
  47. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +24 -0
  48. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +1 -0
  49. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +27 -0
  50. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +1 -0
  51. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +408 -0
  52. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +1 -0
  53. package/fesm2022/tinya-code-gas-admin-ui-form-view-CEe7aZ4m.mjs +4516 -0
  54. package/fesm2022/tinya-code-gas-admin-ui-form-view-CEe7aZ4m.mjs.map +1 -0
  55. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +82 -0
  56. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +1 -0
  57. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +105 -0
  58. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +1 -0
  59. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +65 -0
  60. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +1 -0
  61. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +65 -0
  62. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +1 -0
  63. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +65 -0
  64. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +1 -0
  65. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +59 -0
  66. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +1 -0
  67. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +94 -0
  68. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +1 -0
  69. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +72 -0
  70. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +1 -0
  71. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +238 -0
  72. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +1 -0
  73. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +416 -0
  74. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +1 -0
  75. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +439 -0
  76. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +1 -0
  77. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +513 -0
  78. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +1 -0
  79. package/fesm2022/tinya-code-gas-admin-ui-list-DTtCYS4d.mjs +637 -0
  80. package/fesm2022/tinya-code-gas-admin-ui-list-DTtCYS4d.mjs.map +1 -0
  81. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +127 -0
  82. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +1 -0
  83. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +251 -0
  84. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +1 -0
  85. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +66 -0
  86. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +1 -0
  87. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +114 -0
  88. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +1 -0
  89. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +116 -0
  90. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +1 -0
  91. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +235 -0
  92. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +1 -0
  93. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +8 -0
  94. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +1 -0
  95. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +124 -0
  96. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +1 -0
  97. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +249 -0
  98. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +1 -0
  99. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +39 -0
  100. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +1 -0
  101. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +309 -0
  102. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +1 -0
  103. package/fesm2022/tinya-code-gas-admin-ui-render-registry-B88Pme-y.mjs +576 -0
  104. package/fesm2022/tinya-code-gas-admin-ui-render-registry-B88Pme-y.mjs.map +1 -0
  105. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +161 -0
  106. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +1 -0
  107. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +11 -0
  108. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +685 -0
  109. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +1 -0
  110. package/fesm2022/tinya-code-gas-admin-ui-seed-DkVZbxPP.mjs +80 -0
  111. package/fesm2022/tinya-code-gas-admin-ui-seed-DkVZbxPP.mjs.map +1 -0
  112. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +296 -0
  113. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +1 -0
  114. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +119 -0
  115. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +1 -0
  116. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +158 -0
  117. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +1 -0
  118. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +26 -0
  119. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +1 -0
  120. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +205 -0
  121. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +1 -0
  122. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +432 -0
  123. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +1 -0
  124. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +36 -0
  125. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +1 -0
  126. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +107 -0
  127. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +1 -0
  128. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +223 -0
  129. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +1 -0
  130. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +107 -0
  131. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +1 -0
  132. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +60 -0
  133. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +1 -0
  134. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +141 -0
  135. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +1 -0
  136. package/fesm2022/tinya-code-gas-admin-ui.mjs +4738 -0
  137. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -0
  138. package/package.json +15 -3
  139. package/types/tinya-code-gas-admin-ui.d.ts +3634 -0
  140. package/ng-package.json +0 -7
  141. package/src/lib/actions/action-registry.spec.ts +0 -52
  142. package/src/lib/actions/action-registry.ts +0 -126
  143. package/src/lib/actions/action-service.spec.ts +0 -215
  144. package/src/lib/actions/action-service.ts +0 -87
  145. package/src/lib/actions/descriptors.spec.ts +0 -155
  146. package/src/lib/actions/descriptors.ts +0 -154
  147. package/src/lib/columns/column-cell.ts +0 -33
  148. package/src/lib/columns/column-defaults.spec.ts +0 -99
  149. package/src/lib/columns/column-defaults.ts +0 -84
  150. package/src/lib/columns/column-format.spec.ts +0 -274
  151. package/src/lib/columns/descriptors.ts +0 -113
  152. package/src/lib/columns/format-badge.ts +0 -51
  153. package/src/lib/columns/format-boolean.ts +0 -58
  154. package/src/lib/columns/format-currency.ts +0 -30
  155. package/src/lib/columns/format-date.ts +0 -35
  156. package/src/lib/columns/format-number.ts +0 -32
  157. package/src/lib/columns/format-text.ts +0 -23
  158. package/src/lib/columns/format-thumbnail.ts +0 -63
  159. package/src/lib/core/adapters/firebase-auth.adapter.ts +0 -6
  160. package/src/lib/core/guards/auth.guard.spec.ts +0 -88
  161. package/src/lib/core/guards/auth.guard.ts +0 -48
  162. package/src/lib/core/guards/unsaved-changes.guard.ts +0 -16
  163. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +0 -182
  164. package/src/lib/core/interceptors/api-error.interceptor.ts +0 -87
  165. package/src/lib/core/models/api.model.ts +0 -104
  166. package/src/lib/core/models/schema-descriptions.ts +0 -118
  167. package/src/lib/core/models/schema.model.ts +0 -1097
  168. package/src/lib/core/registry/composition-validator.spec.ts +0 -358
  169. package/src/lib/core/registry/composition-validator.ts +0 -270
  170. package/src/lib/core/registry/field-validators.spec.ts +0 -199
  171. package/src/lib/core/registry/field-validators.ts +0 -440
  172. package/src/lib/core/registry/render-host.spec.ts +0 -208
  173. package/src/lib/core/registry/render-host.ts +0 -646
  174. package/src/lib/core/registry/render-registry.spec.ts +0 -96
  175. package/src/lib/core/registry/render-registry.ts +0 -312
  176. package/src/lib/core/registry/schema-catalog.ts +0 -156
  177. package/src/lib/core/services/api.service.spec.ts +0 -1142
  178. package/src/lib/core/services/api.service.ts +0 -371
  179. package/src/lib/core/services/auth.service.spec.ts +0 -653
  180. package/src/lib/core/services/auth.service.ts +0 -503
  181. package/src/lib/core/services/config.service.spec.ts +0 -121
  182. package/src/lib/core/services/config.service.ts +0 -119
  183. package/src/lib/core/services/data-source.service.spec.ts +0 -179
  184. package/src/lib/core/services/data-source.service.ts +0 -142
  185. package/src/lib/core/services/error-mapper.service.spec.ts +0 -50
  186. package/src/lib/core/services/error-mapper.service.ts +0 -49
  187. package/src/lib/core/services/notification.service.ts +0 -73
  188. package/src/lib/core/services/relation-drawer.service.ts +0 -41
  189. package/src/lib/core/services/theme.service.spec.ts +0 -106
  190. package/src/lib/core/services/theme.service.ts +0 -86
  191. package/src/lib/core/services/transport.spec.ts +0 -198
  192. package/src/lib/core/services/transport.ts +0 -164
  193. package/src/lib/core/services/upload.service.ts +0 -81
  194. package/src/lib/core/testing/categories.schema.ts +0 -125
  195. package/src/lib/core/testing/orders.schema.ts +0 -372
  196. package/src/lib/core/testing/products.schema.ts +0 -258
  197. package/src/lib/core/testing/site.schema.ts +0 -195
  198. package/src/lib/core/testing/test-catalog.ts +0 -21
  199. package/src/lib/core/tokens.ts +0 -112
  200. package/src/lib/core/utils/clipboard.spec.ts +0 -55
  201. package/src/lib/core/utils/clipboard.ts +0 -90
  202. package/src/lib/core/utils/context-resolver.spec.ts +0 -95
  203. package/src/lib/core/utils/context-resolver.ts +0 -101
  204. package/src/lib/core/utils/response-unwrapper.spec.ts +0 -70
  205. package/src/lib/core/utils/response-unwrapper.ts +0 -103
  206. package/src/lib/core/utils/source-options.spec.ts +0 -96
  207. package/src/lib/core/utils/source-options.ts +0 -54
  208. package/src/lib/core/utils/template-interpolator.spec.ts +0 -59
  209. package/src/lib/core/utils/template-interpolator.ts +0 -56
  210. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +0 -207
  211. package/src/lib/core/view-navigation/view-navigation.service.ts +0 -132
  212. package/src/lib/fields/boolean/boolean.descriptor.ts +0 -11
  213. package/src/lib/fields/boolean/boolean.ts +0 -40
  214. package/src/lib/fields/color/color.descriptor.ts +0 -12
  215. package/src/lib/fields/color/color.ts +0 -34
  216. package/src/lib/fields/currency/currency.descriptor.ts +0 -12
  217. package/src/lib/fields/currency/currency.ts +0 -67
  218. package/src/lib/fields/date/date.descriptor.ts +0 -12
  219. package/src/lib/fields/date/date.ts +0 -36
  220. package/src/lib/fields/descriptors.ts +0 -122
  221. package/src/lib/fields/display/display-badge.ts +0 -65
  222. package/src/lib/fields/display/display-date.ts +0 -39
  223. package/src/lib/fields/display/display-defaults.ts +0 -84
  224. package/src/lib/fields/display/display-file.ts +0 -41
  225. package/src/lib/fields/display/display-image.ts +0 -54
  226. package/src/lib/fields/display/display-list.ts +0 -107
  227. package/src/lib/fields/display/display-number.ts +0 -41
  228. package/src/lib/fields/display/display-text.ts +0 -42
  229. package/src/lib/fields/display/display.spec.ts +0 -242
  230. package/src/lib/fields/dropdown/dropdown.descriptor.ts +0 -12
  231. package/src/lib/fields/dropdown/dropdown.ts +0 -45
  232. package/src/lib/fields/email/email.descriptor.ts +0 -12
  233. package/src/lib/fields/email/email.ts +0 -28
  234. package/src/lib/fields/field-aria.ts +0 -60
  235. package/src/lib/fields/field-host/field-host.spec.ts +0 -118
  236. package/src/lib/fields/field-host/field-host.ts +0 -47
  237. package/src/lib/fields/field-node.ts +0 -88
  238. package/src/lib/fields/file/file.descriptor.ts +0 -14
  239. package/src/lib/fields/file/file.spec.ts +0 -134
  240. package/src/lib/fields/file/file.ts +0 -281
  241. package/src/lib/fields/group/group.descriptor.ts +0 -21
  242. package/src/lib/fields/group/group.ts +0 -110
  243. package/src/lib/fields/image/image.descriptor.ts +0 -12
  244. package/src/lib/fields/image/image.spec.ts +0 -117
  245. package/src/lib/fields/image/image.ts +0 -281
  246. package/src/lib/fields/key-value/key-value.descriptor.ts +0 -11
  247. package/src/lib/fields/key-value/key-value.ts +0 -283
  248. package/src/lib/fields/list/list.descriptor.ts +0 -21
  249. package/src/lib/fields/list/list.ts +0 -376
  250. package/src/lib/fields/multiselect/multiselect.descriptor.ts +0 -11
  251. package/src/lib/fields/multiselect/multiselect.spec.ts +0 -142
  252. package/src/lib/fields/multiselect/multiselect.ts +0 -152
  253. package/src/lib/fields/number/number.descriptor.ts +0 -12
  254. package/src/lib/fields/number/number.ts +0 -40
  255. package/src/lib/fields/phone/phone.descriptor.ts +0 -12
  256. package/src/lib/fields/phone/phone.ts +0 -46
  257. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +0 -12
  258. package/src/lib/fields/readonly-text/readonly-text.ts +0 -58
  259. package/src/lib/fields/relation/relation.descriptor.ts +0 -12
  260. package/src/lib/fields/relation/relation.spec.ts +0 -193
  261. package/src/lib/fields/relation/relation.ts +0 -224
  262. package/src/lib/fields/rich-select/rich-select.descriptor.ts +0 -12
  263. package/src/lib/fields/rich-select/rich-select.ts +0 -175
  264. package/src/lib/fields/select/radio-mode.ts +0 -25
  265. package/src/lib/fields/select/select.descriptor.ts +0 -12
  266. package/src/lib/fields/select/select.spec.ts +0 -162
  267. package/src/lib/fields/select/select.ts +0 -210
  268. package/src/lib/fields/slug/slug.descriptor.ts +0 -19
  269. package/src/lib/fields/slug/slug.ts +0 -67
  270. package/src/lib/fields/string-list/string-list.descriptor.ts +0 -11
  271. package/src/lib/fields/string-list/string-list.ts +0 -222
  272. package/src/lib/fields/text/text.descriptor.ts +0 -12
  273. package/src/lib/fields/text/text.ts +0 -35
  274. package/src/lib/fields/textarea/textarea.descriptor.ts +0 -12
  275. package/src/lib/fields/textarea/textarea.spec.ts +0 -117
  276. package/src/lib/fields/textarea/textarea.ts +0 -126
  277. package/src/lib/fields/time/time.descriptor.ts +0 -12
  278. package/src/lib/fields/time/time.ts +0 -28
  279. package/src/lib/fields/unknown-render/unknown-render.ts +0 -49
  280. package/src/lib/fields/url/url.descriptor.ts +0 -12
  281. package/src/lib/fields/url/url.ts +0 -53
  282. package/src/lib/gas-admin-ui.spec.ts +0 -21
  283. package/src/lib/gas-admin-ui.ts +0 -9
  284. package/src/lib/layouts/descriptors.ts +0 -9
  285. package/src/lib/pages/dashboard/dashboard.ts +0 -167
  286. package/src/lib/pages/detail-view/detail-view.spec.ts +0 -333
  287. package/src/lib/pages/detail-view/detail-view.ts +0 -570
  288. package/src/lib/pages/drawer-form/drawer-form.ts +0 -228
  289. package/src/lib/pages/form-view/form-model.ts +0 -88
  290. package/src/lib/pages/form-view/form-schema.ts +0 -255
  291. package/src/lib/pages/form-view/form-view.ts +0 -1622
  292. package/src/lib/pages/list-view/list-view.formats.spec.ts +0 -214
  293. package/src/lib/pages/list-view/list-view.ts +0 -882
  294. package/src/lib/pages/login/login.spec.ts +0 -109
  295. package/src/lib/pages/login/login.ts +0 -227
  296. package/src/lib/pages/resource-page/resource-page.ts +0 -234
  297. package/src/lib/shared/components/badge/badge.ts +0 -35
  298. package/src/lib/shared/components/button/button.ts +0 -65
  299. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +0 -48
  300. package/src/lib/shared/components/copy-button/copy-button.ts +0 -56
  301. package/src/lib/shared/components/copy-share/copy-share.spec.ts +0 -122
  302. package/src/lib/shared/components/copy-share/copy-share.ts +0 -107
  303. package/src/lib/shared/components/drawer/drawer.spec.ts +0 -127
  304. package/src/lib/shared/components/drawer/drawer.ts +0 -172
  305. package/src/lib/shared/components/empty-state/empty-state.ts +0 -30
  306. package/src/lib/shared/components/modal/modal.spec.ts +0 -149
  307. package/src/lib/shared/components/modal/modal.ts +0 -145
  308. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +0 -165
  309. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +0 -159
  310. package/src/lib/shared/components/skeleton/skeleton.ts +0 -31
  311. package/src/lib/shared/components/toast/toast.ts +0 -58
  312. package/src/lib/shared/utils/col-span.ts +0 -25
  313. package/src/lib/shared/utils/condition-evaluator.ts +0 -53
  314. package/src/lib/shared/utils/depends-on.ts +0 -48
  315. package/src/lib/shared/utils/dialog-focus.ts +0 -28
  316. package/src/lib/shared/utils/field-path.ts +0 -49
  317. package/src/lib/shared/utils/gap-sorting.ts +0 -144
  318. package/src/lib/shared/utils/seed.spec.ts +0 -64
  319. package/src/lib/shared/utils/seed.ts +0 -149
  320. package/src/lib/shared/utils/slugify.ts +0 -13
  321. package/src/lib/shared/utils/validators.ts +0 -41
  322. package/src/lib/shell/layout/layout.ts +0 -63
  323. package/src/lib/shell/shell.routes.spec.ts +0 -59
  324. package/src/lib/shell/shell.routes.ts +0 -122
  325. package/src/lib/shell/sidebar/sidebar.ts +0 -139
  326. package/src/lib/shell/theme-toggle/theme-toggle.ts +0 -74
  327. package/src/lib/views/calendar-view/calendar-model.ts +0 -426
  328. package/src/lib/views/calendar-view/calendar-view.spec.ts +0 -557
  329. package/src/lib/views/calendar-view/calendar-view.ts +0 -568
  330. package/src/lib/views/composer/composer-host.spec.ts +0 -301
  331. package/src/lib/views/composer/composer-host.ts +0 -248
  332. package/src/lib/views/composer/composer-imports.spec.ts +0 -54
  333. package/src/lib/views/composer/view-context.spec.ts +0 -117
  334. package/src/lib/views/composer/view-context.ts +0 -109
  335. package/src/lib/views/composer/view-slot.spec.ts +0 -130
  336. package/src/lib/views/composer/view-slot.ts +0 -178
  337. package/src/lib/views/descriptors.ts +0 -46
  338. package/src/lib/views/kanban-view/kanban-model.ts +0 -173
  339. package/src/lib/views/kanban-view/kanban-view.spec.ts +0 -540
  340. package/src/lib/views/kanban-view/kanban-view.ts +0 -327
  341. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +0 -17
  342. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +0 -66
  343. package/src/lib/widgets/bar-chart/bar-chart.ts +0 -76
  344. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +0 -17
  345. package/src/lib/widgets/chart-line/chart-line.spec.ts +0 -113
  346. package/src/lib/widgets/chart-line/chart-line.ts +0 -219
  347. package/src/lib/widgets/descriptors.ts +0 -29
  348. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +0 -41
  349. package/src/lib/widgets/metric-card/metric-card.spec.ts +0 -52
  350. package/src/lib/widgets/metric-card/metric-card.ts +0 -48
  351. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +0 -13
  352. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +0 -61
  353. package/src/lib/widgets/quick-actions/quick-actions.ts +0 -100
  354. package/src/lib/widgets/record-list/record-list.descriptor.ts +0 -16
  355. package/src/lib/widgets/record-list/record-list.spec.ts +0 -79
  356. package/src/lib/widgets/record-list/record-list.ts +0 -115
  357. package/src/lib/widgets/series.spec.ts +0 -42
  358. package/src/lib/widgets/series.ts +0 -56
  359. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +0 -19
  360. package/src/lib/widgets/status-progress/status-progress.spec.ts +0 -63
  361. package/src/lib/widgets/status-progress/status-progress.ts +0 -104
  362. package/src/lib/widgets/widget-host/widget-host.spec.ts +0 -179
  363. package/src/lib/widgets/widget-host/widget-host.ts +0 -99
  364. package/src/public-api.ts +0 -104
  365. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  366. package/tsconfig.lib.json +0 -13
  367. package/tsconfig.lib.prod.json +0 -11
  368. package/tsconfig.spec.json +0 -10
@@ -1,1142 +0,0 @@
1
- import { provideHttpClient } from '@angular/common/http';
2
- import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
3
- import { TestBed } from '@angular/core/testing';
4
- import { provideRouter } from '@angular/router';
5
- import { firstValueFrom } from 'rxjs';
6
-
7
- import { API_DRIVER, API_URL, VALUE_SOURCES, FIREBASE_AUTH_ADAPTER, DEFAULT_RESPONSE_UNWRAP } from '../tokens';
8
- import type { FirebaseAuthAdapter } from '../tokens';
9
- import { ApiService } from './api.service';
10
- import { AuthService, SESSION_TOKEN_KEY, SESSION_USER_KEY } from './auth.service';
11
- import { ConfigService } from './config.service';
12
-
13
- interface Row {
14
- slug: string;
15
- }
16
-
17
- const ENDPOINTS = { list: '/admin/products' };
18
-
19
- describe('ApiService — desarrollo del envelope de listado (F7-3)', () => {
20
- let api: ApiService;
21
- let http: HttpTestingController;
22
-
23
- beforeEach(() => {
24
- // La sesión se restaura desde `sessionStorage` (R2.12): sembrarla antes
25
- // de inyectar es lo mismo que hacer login y recargar la pestaña.
26
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
27
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
28
- TestBed.configureTestingModule({
29
- providers: [
30
- provideHttpClient(),
31
- provideHttpClientTesting(),
32
- provideRouter([]),
33
- { provide: API_URL, useValue: '/api' },
34
- ],
35
- });
36
- api = TestBed.inject(ApiService);
37
- http = TestBed.inject(HttpTestingController);
38
- });
39
-
40
- afterEach(() => {
41
- http.verify();
42
- sessionStorage.clear();
43
- });
44
-
45
- it('pide POST text/plain con el envelope y desenvuelve { items, total }', async () => {
46
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
47
-
48
- const req = http.expectOne('/api');
49
- expect(req.request.method).toBe('POST');
50
- expect(req.request.headers.get('Content-Type')).toBe('text/plain');
51
- expect(req.request.body).toEqual({
52
- token: 'tok',
53
- method: 'GET',
54
- path: '/admin/products',
55
- payload: {},
56
- });
57
- req.flush({ items: [{ slug: 'a' }], total: 1 });
58
-
59
- expect(await pending).toEqual([{ slug: 'a' }]);
60
- });
61
-
62
- it('el sobre estándar R6.4 { items, nextCursor, total } desenvuelve sólo los items', async () => {
63
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
64
- http
65
- .expectOne('/api')
66
- .flush({ items: [{ slug: 'a' }, { slug: 'b' }], nextCursor: 'eyJ2IjoxLCJvIjoyfQ', total: 7 });
67
- expect(await pending).toEqual([{ slug: 'a' }, { slug: 'b' }]);
68
- });
69
-
70
- it('nextCursor null (última página) desenvuelve igual: nadie tiene que comprobarlo', async () => {
71
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
72
- http.expectOne('/api').flush({ items: [{ slug: 'c' }], nextCursor: null, total: 3 });
73
- expect(await pending).toEqual([{ slug: 'c' }]);
74
- });
75
-
76
- it('arreglo plano (contrato histórico, sin envelope) pasa tal cual', async () => {
77
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
78
- http.expectOne('/api').flush([{ slug: 'b' }]);
79
- expect(await pending).toEqual([{ slug: 'b' }]);
80
- });
81
-
82
- it('respuesta inesperada ⇒ lista vacía (defensivo, nunca rompe)', async () => {
83
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
84
- http.expectOne('/api').flush({ otra_clave: true });
85
- expect(await pending).toEqual([]);
86
- });
87
-
88
- it('GET con {key} sustituye el segmento y codifica', async () => {
89
- const pending = firstValueFrom(
90
- api.get<Row>({ ...ENDPOINTS, get: '/admin/products/{key}' }, 'a b'),
91
- );
92
- const req = http.expectOne('/api');
93
- expect((req.request.body as { path: string }).path).toBe('/admin/products/a%20b');
94
- req.flush({ slug: 'a b' });
95
- expect(await pending).toEqual({ slug: 'a b' });
96
- });
97
-
98
- // ── Caracterización (plan doc/context-sources-plan.md · Fase 0) ──────────
99
- // Congelan el request ACTUAL con paths sin marcadores: son la base
100
- // diferencial (R9) que debe seguir coincidiendo byte a byte.
101
- it('get sin {key} (singleton): la path viaja literal dentro del envelope', async () => {
102
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
103
-
104
- const req = http.expectOne('/api');
105
- expect(req.request.method).toBe('POST');
106
- expect(req.request.headers.get('Content-Type')).toBe('text/plain');
107
- expect(req.request.body).toEqual({
108
- token: 'tok',
109
- method: 'GET',
110
- path: '/admin/site',
111
- payload: {},
112
- });
113
- req.flush({ slug: 'site' });
114
- expect(await pending).toEqual({ slug: 'site' });
115
- });
116
-
117
- it('create: payload declarado dentro del envelope con method POST', async () => {
118
- const pending = firstValueFrom(
119
- api.create<Row>({ ...ENDPOINTS, create: '/admin/products' }, { slug: 'nuevo' }),
120
- );
121
-
122
- const req = http.expectOne('/api');
123
- expect(req.request.method).toBe('POST');
124
- expect(req.request.body).toEqual({
125
- token: 'tok',
126
- method: 'POST',
127
- path: '/admin/products',
128
- payload: { slug: 'nuevo' },
129
- });
130
- req.flush({ slug: 'nuevo' });
131
- expect(await pending).toEqual({ slug: 'nuevo' });
132
- });
133
-
134
- it('update sin {key}: conserva la path literal y manda PUT en el envelope', async () => {
135
- const pending = firstValueFrom(
136
- api.update<Row>({ update: '/admin/site' }, undefined, { slug: 'cambiado' }),
137
- );
138
-
139
- const req = http.expectOne('/api');
140
- expect(req.request.method).toBe('POST');
141
- expect(req.request.body).toEqual({
142
- token: 'tok',
143
- method: 'PUT',
144
- path: '/admin/site',
145
- payload: { slug: 'cambiado' },
146
- });
147
- req.flush({ slug: 'cambiado' });
148
- expect(await pending).toEqual({ slug: 'cambiado' });
149
- });
150
-
151
- it('remove sin {key}: DELETE con la path literal en el envelope', async () => {
152
- const pending = firstValueFrom(api.remove({ remove: '/admin/site' }));
153
-
154
- const req = http.expectOne('/api');
155
- expect(req.request.method).toBe('POST');
156
- expect(req.request.body).toEqual({
157
- token: 'tok',
158
- method: 'DELETE',
159
- path: '/admin/site',
160
- payload: {},
161
- });
162
- req.flush(null);
163
- await pending;
164
- });
165
-
166
- it('request: operación genérica conserva método, path y payload declarados', async () => {
167
- const pending = firstValueFrom(
168
- api.request<{ ok: boolean }>('PUT', '/admin/orders', { reorder: ['a', 'b'] }),
169
- );
170
-
171
- const req = http.expectOne('/api');
172
- expect(req.request.method).toBe('POST');
173
- expect(req.request.body).toEqual({
174
- token: 'tok',
175
- method: 'PUT',
176
- path: '/admin/orders',
177
- payload: { reorder: ['a', 'b'] },
178
- });
179
- req.flush({ ok: true });
180
- expect(await pending).toEqual({ ok: true });
181
- });
182
- });
183
-
184
- describe('ApiService — REST driver', () => {
185
- let api: ApiService;
186
- let http: HttpTestingController;
187
-
188
- beforeEach(() => {
189
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
190
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
191
- TestBed.configureTestingModule({
192
- providers: [
193
- provideHttpClient(),
194
- provideHttpClientTesting(),
195
- provideRouter([]),
196
- { provide: API_URL, useValue: '/api' },
197
- { provide: API_DRIVER, useValue: 'rest' },
198
- ],
199
- });
200
- api = TestBed.inject(ApiService);
201
- http = TestBed.inject(HttpTestingController);
202
- });
203
-
204
- afterEach(() => {
205
- http.verify();
206
- sessionStorage.clear();
207
- });
208
-
209
- it('list: ejecuta GET a ${apiUrl}${path} con cabecera Authorization y desenvuelve', async () => {
210
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
211
-
212
- const req = http.expectOne('/api/admin/products');
213
- expect(req.request.method).toBe('GET');
214
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
215
- expect(req.request.body).toBeNull();
216
- req.flush({ items: [{ slug: 'item-rest' }] });
217
-
218
- expect(await pending).toEqual([{ slug: 'item-rest' }]);
219
- });
220
-
221
- it('list con payload: serializa claves como query params y aplana objetos anidados', async () => {
222
- const pending = firstValueFrom(
223
- api.list<Row>(ENDPOINTS, { view: 'active', params: { sort: 'asc', limit: 10 } }),
224
- );
225
-
226
- const req = http.expectOne((r) => r.url === '/api/admin/products');
227
- expect(req.request.method).toBe('GET');
228
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
229
- expect(req.request.params.get('view')).toBe('active');
230
- expect(req.request.params.get('sort')).toBe('asc');
231
- expect(req.request.params.get('limit')).toBe('10');
232
- expect(req.request.params.has('params')).toBe(false);
233
- req.flush([{ slug: 'item-filtered' }]);
234
-
235
- expect(await pending).toEqual([{ slug: 'item-filtered' }]);
236
- });
237
-
238
- it('get: ejecuta GET a ${apiUrl}${path} sustituyendo {key}', async () => {
239
- const pending = firstValueFrom(
240
- api.get<Row>({ ...ENDPOINTS, get: '/admin/products/{key}' }, 'item 1'),
241
- );
242
-
243
- const req = http.expectOne('/api/admin/products/item%201');
244
- expect(req.request.method).toBe('GET');
245
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
246
- req.flush({ slug: 'item 1' });
247
-
248
- expect(await pending).toEqual({ slug: 'item 1' });
249
- });
250
-
251
- it('create: ejecuta POST a ${apiUrl}${path} con body JSON y cabeceras correctas', async () => {
252
- const pending = firstValueFrom(
253
- api.create<Row>({ ...ENDPOINTS, create: '/admin/products' }, { slug: 'nuevo' }),
254
- );
255
-
256
- const req = http.expectOne('/api/admin/products');
257
- expect(req.request.method).toBe('POST');
258
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
259
- expect(req.request.headers.get('Content-Type')).toBe('application/json');
260
- expect(req.request.body).toEqual({ slug: 'nuevo' });
261
- req.flush({ slug: 'nuevo' });
262
-
263
- expect(await pending).toEqual({ slug: 'nuevo' });
264
- });
265
-
266
- it('update: ejecuta PUT a ${apiUrl}${path} con {key} resuelta y body JSON', async () => {
267
- const pending = firstValueFrom(
268
- api.update<Row>({ ...ENDPOINTS, update: '/admin/products/{key}' }, 'prod-1', { slug: 'modificado' }),
269
- );
270
-
271
- const req = http.expectOne('/api/admin/products/prod-1');
272
- expect(req.request.method).toBe('PUT');
273
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
274
- expect(req.request.headers.get('Content-Type')).toBe('application/json');
275
- expect(req.request.body).toEqual({ slug: 'modificado' });
276
- req.flush({ slug: 'modificado' });
277
-
278
- expect(await pending).toEqual({ slug: 'modificado' });
279
- });
280
-
281
- it('remove: ejecuta DELETE a ${apiUrl}${path} con {key} resuelta y Authorization', async () => {
282
- const pending = firstValueFrom(
283
- api.remove({ ...ENDPOINTS, remove: '/admin/products/{key}' }, 'prod-1'),
284
- );
285
-
286
- const req = http.expectOne('/api/admin/products/prod-1');
287
- expect(req.request.method).toBe('DELETE');
288
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
289
- req.flush(null);
290
-
291
- await pending;
292
- });
293
-
294
- it('renovación deslizante: adopta meta.renewedToken en modo REST', async () => {
295
- const auth = TestBed.inject(AuthService);
296
- const pending = firstValueFrom(api.get<Row>({ ...ENDPOINTS, get: '/admin/products/{key}' }, '1'));
297
-
298
- const req = http.expectOne('/api/admin/products/1');
299
- req.flush({ slug: '1', meta: { renewedToken: 'nuevo-token-rest' } });
300
-
301
- await pending;
302
- expect(auth.token()).toBe('nuevo-token-rest');
303
- expect(sessionStorage.getItem(SESSION_TOKEN_KEY)).toBe('nuevo-token-rest');
304
- });
305
-
306
- // ── Caracterización (plan doc/context-sources-plan.md · Fase 0) ──────────
307
- // Misma base diferencial (R9) en modo REST: paths sin marcadores.
308
- it('get sin {key} (singleton): GET a ${apiUrl}${path} con Authorization', async () => {
309
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
310
-
311
- const req = http.expectOne('/api/admin/site');
312
- expect(req.request.method).toBe('GET');
313
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
314
- expect(req.request.body).toBeNull();
315
- req.flush({ slug: 'site' });
316
- expect(await pending).toEqual({ slug: 'site' });
317
- });
318
-
319
- it('update sin {key}: PUT a la path literal con body JSON', async () => {
320
- const pending = firstValueFrom(
321
- api.update<Row>({ update: '/admin/site' }, undefined, { slug: 'cambiado' }),
322
- );
323
-
324
- const req = http.expectOne('/api/admin/site');
325
- expect(req.request.method).toBe('PUT');
326
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
327
- expect(req.request.headers.get('Content-Type')).toBe('application/json');
328
- expect(req.request.body).toEqual({ slug: 'cambiado' });
329
- req.flush({ slug: 'cambiado' });
330
- expect(await pending).toEqual({ slug: 'cambiado' });
331
- });
332
-
333
- it('remove sin {key}: DELETE a la path literal con Authorization', async () => {
334
- const pending = firstValueFrom(api.remove({ remove: '/admin/site' }));
335
-
336
- const req = http.expectOne('/api/admin/site');
337
- expect(req.request.method).toBe('DELETE');
338
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
339
- req.flush(null);
340
- await pending;
341
- });
342
-
343
- it('request: operación genérica respeta método, URL y headers declarados', async () => {
344
- const pending = firstValueFrom(
345
- api.request<{ ok: boolean }>('PATCH', '/admin/site', { campo: 'valor' }),
346
- );
347
-
348
- const req = http.expectOne('/api/admin/site');
349
- expect(req.request.method).toBe('PATCH');
350
- expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
351
- expect(req.request.headers.get('Content-Type')).toBe('application/json');
352
- expect(req.request.body).toEqual({ campo: 'valor' });
353
- req.flush({ ok: true });
354
- expect(await pending).toEqual({ ok: true });
355
- });
356
- });
357
-
358
- // ─────────────────────────────────────────────────────────────────────────
359
- // Firebase ID token refresh (plan doc/firebase-token-refresh-plan.md · Fase 2)
360
- // ─────────────────────────────────────────────────────────────────────────
361
- describe('ApiService — Firebase ID token refresh · driver rest', () => {
362
- let api: ApiService;
363
- let http: HttpTestingController;
364
- let getIdTokenSpy: ReturnType<typeof vi.fn>;
365
-
366
- beforeEach(() => {
367
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
368
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
369
- getIdTokenSpy = vi.fn();
370
- const firebaseAdapter = {
371
- signInWithGoogle: async () => 'google-id-token',
372
- signInWithEmailAndPassword: async () => 'email-id-token',
373
- signOut: async () => undefined,
374
- getIdToken: getIdTokenSpy,
375
- } as unknown as FirebaseAuthAdapter;
376
- TestBed.configureTestingModule({
377
- providers: [
378
- provideHttpClient(),
379
- provideHttpClientTesting(),
380
- provideRouter([]),
381
- { provide: API_URL, useValue: '/api' },
382
- { provide: API_DRIVER, useValue: 'rest' },
383
- { provide: FIREBASE_AUTH_ADAPTER, useValue: firebaseAdapter },
384
- ],
385
- });
386
- api = TestBed.inject(ApiService);
387
- http = TestBed.inject(HttpTestingController);
388
- });
389
-
390
- afterEach(() => {
391
- http.verify();
392
- sessionStorage.clear();
393
- });
394
-
395
- it('llama a getIdToken() antes de enviar y usa el valor refrescado en Authorization', async () => {
396
- getIdTokenSpy.mockResolvedValue('refreshed-id-token');
397
-
398
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
399
-
400
- // Flush microtasks: from(promise) → switchMap → doRestRequest → http.request
401
- await Promise.resolve();
402
- await Promise.resolve();
403
-
404
- expect(getIdTokenSpy).toHaveBeenCalledTimes(1);
405
-
406
- const req = http.expectOne('/api/admin/site');
407
- expect(req.request.method).toBe('GET');
408
- expect(req.request.headers.get('Authorization')).toBe('Bearer refreshed-id-token');
409
- req.flush({ slug: 'site' });
410
- expect(await pending).toEqual({ slug: 'site' });
411
- });
412
-
413
- it('getIdToken() que lanza ⇒ error en el canal de errores y ninguna petición sale', async () => {
414
- getIdTokenSpy.mockRejectedValue(new Error('no hay usuario de Firebase'));
415
-
416
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
417
-
418
- await expect(pending).rejects.toThrow('no hay usuario de Firebase');
419
- http.expectNone(() => true);
420
- });
421
- });
422
-
423
- // ─────────────────────────────────────────────────────────────────────────
424
- // Marcadores de contexto (plan doc/context-sources-plan.md · Fase 3)
425
- // ─────────────────────────────────────────────────────────────────────
426
- describe('ApiService — marcadores de contexto · driver gas', () => {
427
- let api: ApiService;
428
- let http: HttpTestingController;
429
-
430
- /** Path del envelope GAS: el cuerpo es { token, method, path, payload }. */
431
- const pathDe = (req: { request: { body: unknown } }): string =>
432
- (req.request.body as { path: string }).path;
433
-
434
- beforeEach(() => {
435
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
436
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
437
- TestBed.configureTestingModule({
438
- providers: [
439
- provideHttpClient(),
440
- provideHttpClientTesting(),
441
- provideRouter([]),
442
- { provide: API_URL, useValue: '/api' },
443
- { provide: VALUE_SOURCES, useValue: { tenant: () => 'acme' } },
444
- ],
445
- });
446
- api = TestBed.inject(ApiService);
447
- http = TestBed.inject(HttpTestingController);
448
- });
449
-
450
- afterEach(() => {
451
- http.verify();
452
- sessionStorage.clear();
453
- });
454
-
455
- it('list: resuelve {auth.user.id} dentro del path del envelope', async () => {
456
- const pending = firstValueFrom(api.list<Row>({ list: '/businesses/{auth.user.id}/orders' }));
457
-
458
- const req = http.expectOne('/api');
459
- expect(pathDe(req)).toBe('/businesses/u_1/orders');
460
- req.flush([]);
461
- expect(await pending).toEqual([]);
462
- });
463
-
464
- it('get: resuelve la fuente de contexto Y {key} en el mismo path', async () => {
465
- const pending = firstValueFrom(
466
- api.get<Row>({ get: '/businesses/{auth.user.id}/orders/{key}' }, 'o 1'),
467
- );
468
-
469
- const req = http.expectOne('/api');
470
- expect(pathDe(req)).toBe('/businesses/u_1/orders/o%201');
471
- req.flush({ slug: 'o 1' });
472
- await pending;
473
- });
474
-
475
- it('create: el marcador se resuelve también en operaciones sin clave', async () => {
476
- const pending = firstValueFrom(
477
- api.create<Row>({ create: '/businesses/{auth.user.id}/orders' }, { slug: 'nuevo' }),
478
- );
479
-
480
- const req = http.expectOne('/api');
481
- expect(pathDe(req)).toBe('/businesses/u_1/orders');
482
- req.flush({ slug: 'nuevo' });
483
- await pending;
484
- });
485
-
486
- it('update: resuelve marcador y clave juntos', async () => {
487
- const pending = firstValueFrom(
488
- api.update<Row>({ update: '/businesses/{auth.user.id}/orders/{key}' }, 'o1', { slug: 'x' }),
489
- );
490
-
491
- const req = http.expectOne('/api');
492
- expect(pathDe(req)).toBe('/businesses/u_1/orders/o1');
493
- req.flush({ slug: 'x' });
494
- await pending;
495
- });
496
-
497
- it('remove: resuelve marcador y clave juntos', async () => {
498
- const pending = firstValueFrom(
499
- api.remove({ remove: '/businesses/{auth.user.id}/orders/{key}' }, 'o1'),
500
- );
501
-
502
- const req = http.expectOne('/api');
503
- expect(pathDe(req)).toBe('/businesses/u_1/orders/o1');
504
- req.flush(null);
505
- await pending;
506
- });
507
-
508
- it('request: operación genérica resuelve marcadores de fuentes registradas', async () => {
509
- const pending = firstValueFrom(
510
- api.request<{ ok: boolean }>('PUT', '/tenants/{tenant}/reorder', { orden: [1, 2] }),
511
- );
512
-
513
- const req = http.expectOne('/api');
514
- expect(pathDe(req)).toBe('/tenants/acme/reorder');
515
- req.flush({ ok: true });
516
- await pending;
517
- });
518
-
519
- it('config: {config.public_base_url} se lee de forma perezosa, sin ciclo de DI (R2)', async () => {
520
- const esperado = String(TestBed.inject(ConfigService).globalConfig()['public_base_url']);
521
- const pending = firstValueFrom(api.list<Row>({ list: '/x/{config.public_base_url}' }));
522
-
523
- const req = http.expectOne('/api');
524
- expect(pathDe(req)).toBe(`/x/${encodeURIComponent(esperado)}`);
525
- req.flush([]);
526
- await pending;
527
- });
528
-
529
- it('fuente no registrada ⇒ error en el canal de errores y NINGUNA petición', async () => {
530
- const pending = firstValueFrom(api.list<Row>({ list: '/x/{desconocida.id}/y' }));
531
-
532
- await expect(pending).rejects.toThrow('desconocida');
533
- http.expectNone(() => true);
534
- });
535
-
536
- it('valor ausente ⇒ error en el canal de errores y NINGUNA petición', async () => {
537
- const pending = firstValueFrom(api.list<Row>({ list: '/x/{auth.user.nope}' }));
538
-
539
- await expect(pending).rejects.toThrow('auth.user.nope');
540
- http.expectNone(() => true);
541
- });
542
- });
543
-
544
- describe('ApiService — marcadores de contexto · driver rest', () => {
545
- let api: ApiService;
546
- let http: HttpTestingController;
547
-
548
- beforeEach(() => {
549
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
550
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
551
- TestBed.configureTestingModule({
552
- providers: [
553
- provideHttpClient(),
554
- provideHttpClientTesting(),
555
- provideRouter([]),
556
- { provide: API_URL, useValue: '/api' },
557
- { provide: API_DRIVER, useValue: 'rest' },
558
- { provide: VALUE_SOURCES, useValue: { tenant: () => 'acme' } },
559
- ],
560
- });
561
- api = TestBed.inject(ApiService);
562
- http = TestBed.inject(HttpTestingController);
563
- });
564
-
565
- afterEach(() => {
566
- http.verify();
567
- sessionStorage.clear();
568
- });
569
-
570
- it('list: el marcador queda resuelto en la URL saliente', async () => {
571
- const pending = firstValueFrom(api.list<Row>({ list: '/businesses/{auth.user.id}/orders' }));
572
-
573
- const req = http.expectOne('/api/businesses/u_1/orders');
574
- expect(req.request.method).toBe('GET');
575
- req.flush([]);
576
- expect(await pending).toEqual([]);
577
- });
578
-
579
- it('get: resuelve la fuente de contexto Y {key} en la URL', async () => {
580
- const pending = firstValueFrom(
581
- api.get<Row>({ get: '/businesses/{auth.user.id}/orders/{key}' }, 'o 1'),
582
- );
583
-
584
- const req = http.expectOne('/api/businesses/u_1/orders/o%201');
585
- expect(req.request.method).toBe('GET');
586
- req.flush({ slug: 'o 1' });
587
- await pending;
588
- });
589
-
590
- it('create: POST a la URL con el marcador resuelto', async () => {
591
- const pending = firstValueFrom(
592
- api.create<Row>({ create: '/businesses/{auth.user.id}/orders' }, { slug: 'nuevo' }),
593
- );
594
-
595
- const req = http.expectOne('/api/businesses/u_1/orders');
596
- expect(req.request.method).toBe('POST');
597
- req.flush({ slug: 'nuevo' });
598
- await pending;
599
- });
600
-
601
- it('update: PUT con marcador y clave resueltos', async () => {
602
- const pending = firstValueFrom(
603
- api.update<Row>({ update: '/businesses/{auth.user.id}/orders/{key}' }, 'o1', { slug: 'x' }),
604
- );
605
-
606
- const req = http.expectOne('/api/businesses/u_1/orders/o1');
607
- expect(req.request.method).toBe('PUT');
608
- req.flush({ slug: 'x' });
609
- await pending;
610
- });
611
-
612
- it('remove: DELETE con marcador y clave resueltos', async () => {
613
- const pending = firstValueFrom(
614
- api.remove({ remove: '/businesses/{auth.user.id}/orders/{key}' }, 'o1'),
615
- );
616
-
617
- const req = http.expectOne('/api/businesses/u_1/orders/o1');
618
- expect(req.request.method).toBe('DELETE');
619
- req.flush(null);
620
- await pending;
621
- });
622
-
623
- it('request: operación genérica resuelve fuentes registradas por el consumidor', async () => {
624
- const pending = firstValueFrom(
625
- api.request<{ ok: boolean }>('PATCH', '/tenants/{tenant}/reorder', { orden: [1, 2] }),
626
- );
627
-
628
- const req = http.expectOne('/api/tenants/acme/reorder');
629
- expect(req.request.method).toBe('PATCH');
630
- req.flush({ ok: true });
631
- await pending;
632
- });
633
- });
634
-
635
- describe('ApiService — precedencia de fuentes (documentado en el README)', () => {
636
- let api: ApiService;
637
- let http: HttpTestingController;
638
-
639
- beforeEach(() => {
640
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
641
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
642
- TestBed.configureTestingModule({
643
- providers: [
644
- provideHttpClient(),
645
- provideHttpClientTesting(),
646
- provideRouter([]),
647
- { provide: API_URL, useValue: '/api' },
648
- // El registro intenta pisar `auth`: el motor es dueño de sus fuentes.
649
- { provide: VALUE_SOURCES, useValue: { auth: () => ({ user: { id: 'pirata' } }) } },
650
- ],
651
- });
652
- api = TestBed.inject(ApiService);
653
- http = TestBed.inject(HttpTestingController);
654
- });
655
-
656
- afterEach(() => {
657
- http.verify();
658
- sessionStorage.clear();
659
- });
660
-
661
- it('valueSources no puede sombrear `auth` ni `config`: manda la fuente del motor', async () => {
662
- const pending = firstValueFrom(api.list<Row>({ list: '/x/{auth.user.id}' }));
663
-
664
- const req = http.expectOne('/api');
665
- expect((req.request.body as { path: string }).path).toBe('/x/u_1');
666
- req.flush([]);
667
- await pending;
668
- });
669
- });
670
-
671
- // ─────────────────────────────────────────────────────────────────────────
672
- // Respuesta: caracterización de unwrapping (plan doc/engine-flexibility-plan.md · Fase 0)
673
- // Congela el comportamiento ACTUAL de respuesta para ser la base diferencial (R9)
674
- // antes de agregar declaraciones de unwrap y endpoints personalizados.
675
- // ─────────────────────────────────────────────────────────────────────────
676
- describe('ApiService — respuesta: caracterización de unwrapping', () => {
677
- let api: ApiService;
678
- let http: HttpTestingController;
679
-
680
- beforeEach(() => {
681
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
682
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
683
- TestBed.configureTestingModule({
684
- providers: [
685
- provideHttpClient(),
686
- provideHttpClientTesting(),
687
- provideRouter([]),
688
- { provide: API_URL, useValue: '/api' },
689
- { provide: API_DRIVER, useValue: 'rest' },
690
- ],
691
- });
692
- api = TestBed.inject(ApiService);
693
- http = TestBed.inject(HttpTestingController);
694
- });
695
-
696
- afterEach(() => {
697
- http.verify();
698
- sessionStorage.clear();
699
- });
700
-
701
- it('list: respuesta { items } devuelve solo los items (contrato actual)', async () => {
702
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
703
- const req = http.expectOne('/api/admin/products');
704
- expect(req.request.method).toBe('GET');
705
- req.flush({ items: [{ slug: 'a' }, { slug: 'b' }], total: 2 });
706
- expect(await pending).toEqual([{ slug: 'a' }, { slug: 'b' }]);
707
- });
708
-
709
- it('list: { items, nextCursor, total } desenvuelve items y descarta cursor (baseline)', async () => {
710
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
711
- const req = http.expectOne('/api/admin/products');
712
- req.flush({ items: [{ slug: 'a' }], nextCursor: 'tok', total: 1 });
713
- expect(await pending).toEqual([{ slug: 'a' }]);
714
- });
715
-
716
- it('list: arreglo plano pasa tal cual (contrato histórico)', async () => {
717
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
718
- const req = http.expectOne('/api/admin/products');
719
- expect(req.request.method).toBe('GET');
720
- req.flush([{ slug: 'a' }]);
721
- expect(await pending).toEqual([{ slug: 'a' }]);
722
- });
723
-
724
- it('list: respuesta { data } envuelta devuelve [] hoy (unwrapList busca items)', async () => {
725
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
726
- const req = http.expectOne('/api/admin/products');
727
- expect(req.request.method).toBe('GET');
728
- req.flush({ data: [{ slug: 'a' }], meta: { total: 1 } });
729
- expect(await pending).toEqual([]);
730
- });
731
-
732
- it('get: objeto plano devuelve tal cual', async () => {
733
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
734
- const req = http.expectOne('/api/admin/site');
735
- expect(req.request.method).toBe('GET');
736
- req.flush({ slug: 'site', id: '1' });
737
- expect(await pending).toEqual({ slug: 'site', id: '1' });
738
- });
739
-
740
- it('get: respuesta { data } envuelta devuelve el sobre entero hoy', async () => {
741
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
742
- const req = http.expectOne('/api/admin/site');
743
- expect(req.request.method).toBe('GET');
744
- req.flush({ data: { slug: 'site' }, meta: {} });
745
- expect(await pending).toEqual({ data: { slug: 'site' }, meta: {} });
746
- });
747
-
748
- it('create: devuelve el cuerpo raw del servidor', async () => {
749
- const pending = firstValueFrom(
750
- api.create<Row>({ create: '/admin/products' }, { slug: 'nuevo' }),
751
- );
752
- const req = http.expectOne('/api/admin/products');
753
- expect(req.request.method).toBe('POST');
754
- req.flush({ slug: 'nuevo', id: 'p1' });
755
- expect(await pending).toEqual({ slug: 'nuevo', id: 'p1' });
756
- });
757
-
758
- it('update: devuelve el cuerpo raw del servidor', async () => {
759
- const pending = firstValueFrom(
760
- api.update<Row>({ update: '/admin/products/{key}' }, 'p1', { slug: 'x' }),
761
- );
762
- const req = http.expectOne('/api/admin/products/p1');
763
- expect(req.request.method).toBe('PUT');
764
- req.flush({ slug: 'x', id: 'p1' });
765
- expect(await pending).toEqual({ slug: 'x', id: 'p1' });
766
- });
767
-
768
- it('remove: no devuelve contenido (void)', async () => {
769
- const pending = firstValueFrom(
770
- api.remove({ remove: '/admin/products/{key}' }, 'p1'),
771
- );
772
- const req = http.expectOne('/api/admin/products/p1');
773
- expect(req.request.method).toBe('DELETE');
774
- req.flush(null);
775
- await pending;
776
- });
777
- });
778
-
779
- // ─────────────────────────────────────────────────────────────────────────
780
- // Engine flexibility — response unwrapping + custom endpoints (plan Phase 3)
781
- // ─────────────────────────────────────────────────────────────────────────
782
- describe('ApiService — response unwrapping declarado', () => {
783
- let api: ApiService;
784
- let http: HttpTestingController;
785
-
786
- beforeEach(() => {
787
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
788
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
789
- TestBed.configureTestingModule({
790
- providers: [
791
- provideHttpClient(),
792
- provideHttpClientTesting(),
793
- provideRouter([]),
794
- { provide: API_URL, useValue: '/api' },
795
- { provide: API_DRIVER, useValue: 'rest' },
796
- ],
797
- });
798
- api = TestBed.inject(ApiService);
799
- http = TestBed.inject(HttpTestingController);
800
- });
801
-
802
- afterEach(() => {
803
- http.verify();
804
- sessionStorage.clear();
805
- });
806
-
807
- it('list: con responseUnwrap dataKey "data" devuelve el array envuelto', async () => {
808
- const endpoints = { list: '/admin/products', responseUnwrap: { dataKey: 'data', metaKey: 'meta' } };
809
- const pending = firstValueFrom(api.list<Row>(endpoints));
810
- const req = http.expectOne('/api/admin/products');
811
- req.flush({ data: [{ slug: 'a' }, { slug: 'b' }], meta: { total: 2 } });
812
- expect(await pending).toEqual([{ slug: 'a' }, { slug: 'b' }]);
813
- });
814
-
815
- it('get: con responseUnwrap dataKey "data" devuelve el objeto envuelto', async () => {
816
- const endpoints = { get: '/admin/site', responseUnwrap: { dataKey: 'data' } };
817
- const pending = firstValueFrom(api.get<Row>(endpoints));
818
- const req = http.expectOne('/api/admin/site');
819
- req.flush({ data: { slug: 'site' }, meta: {} });
820
- expect(await pending).toEqual({ slug: 'site' });
821
- });
822
-
823
- it('get: clave de unwrap ausente lanza error descriptivo', async () => {
824
- const endpoints = { get: '/admin/site', responseUnwrap: { dataKey: 'data' } };
825
- const pending = firstValueFrom(api.get<Row>(endpoints));
826
- const req = http.expectOne('/api/admin/site');
827
- req.flush({ slug: 'site' });
828
- await expect(pending).rejects.toThrow(/dataKey 'data'/);
829
- });
830
-
831
- it('sin declaración mantiene el comportamiento actual (passthrough)', async () => {
832
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
833
- const req = http.expectOne('/api/admin/site');
834
- req.flush({ slug: 'site', id: '1' });
835
- expect(await pending).toEqual({ slug: 'site', id: '1' });
836
- });
837
- });
838
-
839
- describe('ApiService — endpoints personalizados', () => {
840
- let api: ApiService;
841
- let http: HttpTestingController;
842
-
843
- beforeEach(() => {
844
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
845
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
846
- TestBed.configureTestingModule({
847
- providers: [
848
- provideHttpClient(),
849
- provideHttpClientTesting(),
850
- provideRouter([]),
851
- { provide: API_URL, useValue: '/api' },
852
- { provide: API_DRIVER, useValue: 'rest' },
853
- ],
854
- });
855
- api = TestBed.inject(ApiService);
856
- http = TestBed.inject(HttpTestingController);
857
- });
858
-
859
- afterEach(() => {
860
- http.verify();
861
- sessionStorage.clear();
862
- });
863
-
864
- it('custom endpoint con PATCH envía el verbo correcto', async () => {
865
- const def = { name: 'recalc', method: 'PATCH' as const, path: '/admin/products/{key}/rank' };
866
- const pending = firstValueFrom(api.invokeCustomEndpoint<{ ok: boolean }>(def, { value: 5 }, undefined, 'p1'));
867
- const req = http.expectOne('/api/admin/products/p1/rank');
868
- expect(req.request.method).toBe('PATCH');
869
- expect(req.request.body).toEqual({ value: 5 });
870
- req.flush({ ok: true });
871
- expect(await pending).toEqual({ ok: true });
872
- });
873
-
874
- it('custom endpoint multipart envía FormData sin Content-Type application/json', async () => {
875
- const def = { name: 'upload', method: 'POST' as const, path: '/admin/upload', payloadMode: 'multipart' as const };
876
- const fd = new FormData();
877
- fd.append('file', new Blob(['hello']), 'hello.txt');
878
- const pending = firstValueFrom(api.invokeCustomEndpoint<{ ok: boolean }>(def, fd));
879
- const req = http.expectOne('/api/admin/upload');
880
- expect(req.request.method).toBe('POST');
881
- expect(req.request.body instanceof FormData).toBe(true);
882
- expect(req.request.headers.get('Content-Type')).not.toBe('application/json');
883
- req.flush({ ok: true });
884
- expect(await pending).toEqual({ ok: true });
885
- });
886
-
887
- it('custom endpoint con responseUnwrap aplica el unwrap', async () => {
888
- const def = {
889
- name: 'summary',
890
- method: 'GET' as const,
891
- path: '/admin/summary',
892
- responseUnwrap: { dataKey: 'data' },
893
- };
894
- const pending = firstValueFrom(api.invokeCustomEndpoint<{ total: number }>(def));
895
- const req = http.expectOne('/api/admin/summary');
896
- req.flush({ data: { total: 42 }, meta: {} });
897
- expect(await pending).toEqual({ total: 42 });
898
- });
899
- });
900
-
901
- // ─────────────────────────────────────────────────────────────────────────
902
- // Unwrap global + unwrapList ruidoso (plan doc/engine-refactor-plan.md · Fase 2)
903
- // ─────────────────────────────────────────────────────────────────────────
904
- describe('ApiService — default global de unwrap (Fase 2)', () => {
905
- let api: ApiService;
906
- let http: HttpTestingController;
907
-
908
- beforeEach(() => {
909
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
910
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
911
- TestBed.configureTestingModule({
912
- providers: [
913
- provideHttpClient(),
914
- provideHttpClientTesting(),
915
- provideRouter([]),
916
- { provide: API_URL, useValue: '/api' },
917
- { provide: API_DRIVER, useValue: 'rest' },
918
- { provide: DEFAULT_RESPONSE_UNWRAP, useValue: { dataKey: 'data', metaKey: 'meta' } },
919
- ],
920
- });
921
- api = TestBed.inject(ApiService);
922
- http = TestBed.inject(HttpTestingController);
923
- });
924
-
925
- afterEach(() => {
926
- http.verify();
927
- sessionStorage.clear();
928
- });
929
-
930
- it('list: el default global se aplica aunque el recurso no declare unwrap', async () => {
931
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
932
- const req = http.expectOne('/api/admin/products');
933
- req.flush({ data: [{ slug: 'a' }, { slug: 'b' }], meta: { total: 2 } });
934
- expect(await pending).toEqual([{ slug: 'a' }, { slug: 'b' }]);
935
- });
936
-
937
- it('get: el default global también aplica a get sin declaración por recurso', async () => {
938
- const pending = firstValueFrom(api.get<Row>({ get: '/admin/site' }));
939
- const req = http.expectOne('/api/admin/site');
940
- req.flush({ data: { slug: 'site' }, meta: {} });
941
- expect(await pending).toEqual({ slug: 'site' });
942
- });
943
- });
944
-
945
- describe('ApiService — el recurso pisa el default global (Fase 2)', () => {
946
- let api: ApiService;
947
- let http: HttpTestingController;
948
-
949
- beforeEach(() => {
950
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
951
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
952
- TestBed.configureTestingModule({
953
- providers: [
954
- provideHttpClient(),
955
- provideHttpClientTesting(),
956
- provideRouter([]),
957
- { provide: API_URL, useValue: '/api' },
958
- { provide: API_DRIVER, useValue: 'rest' },
959
- // Default global distinto al del recurso: el recurso debe ganar.
960
- { provide: DEFAULT_RESPONSE_UNWRAP, useValue: { dataKey: 'payload' } },
961
- ],
962
- });
963
- api = TestBed.inject(ApiService);
964
- http = TestBed.inject(HttpTestingController);
965
- });
966
-
967
- afterEach(() => {
968
- http.verify();
969
- sessionStorage.clear();
970
- });
971
-
972
- it('list: el dataKey del recurso tiene prioridad sobre el del default global', async () => {
973
- const endpoints = { list: '/admin/products', responseUnwrap: { dataKey: 'data' } };
974
- const pending = firstValueFrom(api.list<Row>(endpoints));
975
- const req = http.expectOne('/api/admin/products');
976
- req.flush({ data: [{ slug: 'x' }], payload: [{ slug: 'IGNORED' }] });
977
- expect(await pending).toEqual([{ slug: 'x' }]);
978
- });
979
-
980
- it('list: sin declaración por recurso se usa el dataKey del default global', async () => {
981
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
982
- const req = http.expectOne('/api/admin/products');
983
- req.flush({ payload: [{ slug: 'g' }] });
984
- expect(await pending).toEqual([{ slug: 'g' }]);
985
- });
986
- });
987
-
988
- describe('ApiService — unwrapList ruidoso (Fase 2)', () => {
989
- let api: ApiService;
990
- let http: HttpTestingController;
991
-
992
- beforeEach(() => {
993
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
994
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
995
- TestBed.configureTestingModule({
996
- providers: [
997
- provideHttpClient(),
998
- provideHttpClientTesting(),
999
- provideRouter([]),
1000
- { provide: API_URL, useValue: '/api' },
1001
- { provide: API_DRIVER, useValue: 'rest' },
1002
- ],
1003
- });
1004
- api = TestBed.inject(ApiService);
1005
- http = TestBed.inject(HttpTestingController);
1006
- });
1007
-
1008
- afterEach(() => {
1009
- http.verify();
1010
- sessionStorage.clear();
1011
- });
1012
-
1013
- it('list: cuerpo que no es array ni objeto lanza error descriptivo (no devuelve [])', async () => {
1014
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
1015
- const req = http.expectOne('/api/admin/products');
1016
- req.flush('boom');
1017
- await expect(pending).rejects.toThrow(/list/i);
1018
- });
1019
-
1020
- it('list: con { items } sigue funcionando igual (regresión)', async () => {
1021
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
1022
- const req = http.expectOne('/api/admin/products');
1023
- req.flush({ items: [{ slug: 'a' }], total: 1 });
1024
- expect(await pending).toEqual([{ slug: 'a' }]);
1025
- });
1026
-
1027
- it('list: objeto sin claves reconocidas mantiene [] (compat R9 congelada)', async () => {
1028
- const pending = firstValueFrom(api.list<Row>(ENDPOINTS));
1029
- const req = http.expectOne('/api/admin/products');
1030
- req.flush({ data: [{ slug: 'a' }], meta: { total: 1 } });
1031
- expect(await pending).toEqual([]);
1032
- });
1033
- });
1034
-
1035
- // ─────────────────────────────────────────────────────────────────────────
1036
- // listPage() — paginación opt-in (plan doc/engine-refactor-plan.md · Fase 3)
1037
- // ─────────────────────────────────────────────────────────────────────────
1038
- describe('ApiService — listPage (Fase 3)', () => {
1039
- let api: ApiService;
1040
- let http: HttpTestingController;
1041
-
1042
- beforeEach(() => {
1043
- sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
1044
- sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
1045
- TestBed.configureTestingModule({
1046
- providers: [
1047
- provideHttpClient(),
1048
- provideHttpClientTesting(),
1049
- provideRouter([]),
1050
- { provide: API_URL, useValue: '/api' },
1051
- { provide: API_DRIVER, useValue: 'rest' },
1052
- ],
1053
- });
1054
- api = TestBed.inject(ApiService);
1055
- http = TestBed.inject(HttpTestingController);
1056
- });
1057
-
1058
- afterEach(() => {
1059
- http.verify();
1060
- sessionStorage.clear();
1061
- });
1062
-
1063
- it('3.1: devuelve { items, nextCursor, total, hasMore } desde { items, nextCursor, total }', async () => {
1064
- const pending = firstValueFrom(api.listPage<Row>(ENDPOINTS));
1065
- const req = http.expectOne('/api/admin/products');
1066
- expect(req.request.method).toBe('GET');
1067
- req.flush({ items: [{ slug: 'a' }], nextCursor: 'tok1', total: 25 });
1068
- expect(await pending).toEqual({
1069
- items: [{ slug: 'a' }],
1070
- nextCursor: 'tok1',
1071
- total: 25,
1072
- hasMore: true,
1073
- });
1074
- });
1075
-
1076
- it('3.2: con unwrap declarado extrae items de dataKey y paginación de meta', async () => {
1077
- const endpoints = {
1078
- list: '/admin/products',
1079
- responseUnwrap: { dataKey: 'data', metaKey: 'meta' },
1080
- };
1081
- const pending = firstValueFrom(api.listPage<Row>(endpoints));
1082
- const req = http.expectOne('/api/admin/products');
1083
- req.flush({ data: [{ slug: 'a' }], meta: { nextCursor: 'c9', total: 3 } });
1084
- expect(await pending).toEqual({
1085
- items: [{ slug: 'a' }],
1086
- nextCursor: 'c9',
1087
- total: 3,
1088
- hasMore: true,
1089
- });
1090
- });
1091
-
1092
- it('3.3: cursor ausente ⇒ hasMore = false y nextCursor = null', async () => {
1093
- const pending = firstValueFrom(api.listPage<Row>(ENDPOINTS));
1094
- const req = http.expectOne('/api/admin/products');
1095
- req.flush({ items: [{ slug: 'a' }], total: 1 });
1096
- expect(await pending).toEqual({
1097
- items: [{ slug: 'a' }],
1098
- nextCursor: null,
1099
- total: 1,
1100
- hasMore: false,
1101
- });
1102
- });
1103
-
1104
- it('3.5: payload de paginación (cursor, limit) viaja como payload normal en GET', async () => {
1105
- const pending = firstValueFrom(api.listPage<Row>(ENDPOINTS, { cursor: 'c1', limit: 20 }));
1106
- const req = http.expectOne('/api/admin/products?cursor=c1&limit=20');
1107
- expect(req.request.method).toBe('GET');
1108
- req.flush({ items: [], nextCursor: 'c2', total: 40 });
1109
- expect((await pending).nextCursor).toBe('c2');
1110
- });
1111
-
1112
- it('arreglo plano también pagina (hasMore false sin cursor)', async () => {
1113
- const pending = firstValueFrom(api.listPage<Row>(ENDPOINTS));
1114
- const req = http.expectOne('/api/admin/products');
1115
- req.flush([{ slug: 'a' }]);
1116
- expect(await pending).toEqual({
1117
- items: [{ slug: 'a' }],
1118
- nextCursor: null,
1119
- total: 0,
1120
- hasMore: false,
1121
- });
1122
- });
1123
-
1124
- it('claves de paginación custom vía responseUnwrap.pagination', async () => {
1125
- const endpoints = {
1126
- list: '/admin/products',
1127
- responseUnwrap: {
1128
- dataKey: 'results',
1129
- pagination: { cursorKey: 'after', totalKey: 'count', hasMoreKey: 'remaining' },
1130
- },
1131
- };
1132
- const pending = firstValueFrom(api.listPage<Row>(endpoints));
1133
- const req = http.expectOne('/api/admin/products');
1134
- req.flush({ results: [{ slug: 'a' }], after: 'z1', count: 9, remaining: true });
1135
- expect(await pending).toEqual({
1136
- items: [{ slug: 'a' }],
1137
- nextCursor: 'z1',
1138
- total: 9,
1139
- hasMore: true,
1140
- });
1141
- });
1142
- });