@tinya-code/gas-admin-ui 0.2.0-alpha.12 → 0.2.0-alpha.15
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.
- package/README.md +222 -0
- package/ng-package.json +7 -0
- package/package.json +3 -15
- package/src/lib/actions/action-registry.spec.ts +52 -0
- package/src/lib/actions/action-registry.ts +126 -0
- package/src/lib/actions/action-service.spec.ts +215 -0
- package/src/lib/actions/action-service.ts +87 -0
- package/src/lib/actions/descriptors.spec.ts +155 -0
- package/src/lib/actions/descriptors.ts +154 -0
- package/src/lib/columns/column-cell.ts +33 -0
- package/src/lib/columns/column-defaults.spec.ts +99 -0
- package/src/lib/columns/column-defaults.ts +84 -0
- package/src/lib/columns/column-format.spec.ts +274 -0
- package/src/lib/columns/descriptors.ts +113 -0
- package/src/lib/columns/format-badge.ts +51 -0
- package/src/lib/columns/format-boolean.ts +58 -0
- package/src/lib/columns/format-currency.ts +30 -0
- package/src/lib/columns/format-date.ts +35 -0
- package/src/lib/columns/format-number.ts +32 -0
- package/src/lib/columns/format-text.ts +23 -0
- package/src/lib/columns/format-thumbnail.ts +63 -0
- package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
- package/src/lib/core/guards/auth.guard.spec.ts +88 -0
- package/src/lib/core/guards/auth.guard.ts +48 -0
- package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
- package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
- package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
- package/src/lib/core/models/api.model.ts +104 -0
- package/src/lib/core/models/schema-descriptions.ts +118 -0
- package/src/lib/core/models/schema.model.ts +1097 -0
- package/src/lib/core/registry/composition-validator.spec.ts +358 -0
- package/src/lib/core/registry/composition-validator.ts +270 -0
- package/src/lib/core/registry/field-validators.spec.ts +199 -0
- package/src/lib/core/registry/field-validators.ts +440 -0
- package/src/lib/core/registry/render-host.spec.ts +208 -0
- package/src/lib/core/registry/render-host.ts +646 -0
- package/src/lib/core/registry/render-registry.spec.ts +96 -0
- package/src/lib/core/registry/render-registry.ts +312 -0
- package/src/lib/core/registry/schema-catalog.ts +156 -0
- package/src/lib/core/services/api.service.spec.ts +1142 -0
- package/src/lib/core/services/api.service.ts +371 -0
- package/src/lib/core/services/auth.service.spec.ts +653 -0
- package/src/lib/core/services/auth.service.ts +503 -0
- package/src/lib/core/services/config.service.spec.ts +121 -0
- package/src/lib/core/services/config.service.ts +119 -0
- package/src/lib/core/services/data-source.service.spec.ts +179 -0
- package/src/lib/core/services/data-source.service.ts +142 -0
- package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
- package/src/lib/core/services/error-mapper.service.ts +49 -0
- package/src/lib/core/services/notification.service.ts +73 -0
- package/src/lib/core/services/relation-drawer.service.ts +41 -0
- package/src/lib/core/services/theme.service.spec.ts +106 -0
- package/src/lib/core/services/theme.service.ts +86 -0
- package/src/lib/core/services/transport.spec.ts +198 -0
- package/src/lib/core/services/transport.ts +164 -0
- package/src/lib/core/services/upload.service.ts +81 -0
- package/src/lib/core/testing/categories.schema.ts +125 -0
- package/src/lib/core/testing/orders.schema.ts +372 -0
- package/src/lib/core/testing/products.schema.ts +258 -0
- package/src/lib/core/testing/site.schema.ts +195 -0
- package/src/lib/core/testing/test-catalog.ts +21 -0
- package/src/lib/core/tokens.ts +112 -0
- package/src/lib/core/utils/clipboard.spec.ts +55 -0
- package/src/lib/core/utils/clipboard.ts +90 -0
- package/src/lib/core/utils/context-resolver.spec.ts +95 -0
- package/src/lib/core/utils/context-resolver.ts +101 -0
- package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
- package/src/lib/core/utils/response-unwrapper.ts +103 -0
- package/src/lib/core/utils/source-options.spec.ts +96 -0
- package/src/lib/core/utils/source-options.ts +54 -0
- package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
- package/src/lib/core/utils/template-interpolator.ts +56 -0
- package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
- package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
- package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
- package/src/lib/fields/boolean/boolean.ts +40 -0
- package/src/lib/fields/color/color.descriptor.ts +12 -0
- package/src/lib/fields/color/color.ts +34 -0
- package/src/lib/fields/currency/currency.descriptor.ts +12 -0
- package/src/lib/fields/currency/currency.ts +67 -0
- package/src/lib/fields/date/date.descriptor.ts +12 -0
- package/src/lib/fields/date/date.ts +36 -0
- package/src/lib/fields/descriptors.ts +122 -0
- package/src/lib/fields/display/display-badge.ts +65 -0
- package/src/lib/fields/display/display-date.ts +39 -0
- package/src/lib/fields/display/display-defaults.ts +84 -0
- package/src/lib/fields/display/display-file.ts +41 -0
- package/src/lib/fields/display/display-image.ts +54 -0
- package/src/lib/fields/display/display-list.ts +107 -0
- package/src/lib/fields/display/display-number.ts +41 -0
- package/src/lib/fields/display/display-text.ts +42 -0
- package/src/lib/fields/display/display.spec.ts +242 -0
- package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
- package/src/lib/fields/dropdown/dropdown.ts +45 -0
- package/src/lib/fields/email/email.descriptor.ts +12 -0
- package/src/lib/fields/email/email.ts +28 -0
- package/src/lib/fields/field-aria.ts +60 -0
- package/src/lib/fields/field-host/field-host.spec.ts +118 -0
- package/src/lib/fields/field-host/field-host.ts +47 -0
- package/src/lib/fields/field-node.ts +88 -0
- package/src/lib/fields/file/file.descriptor.ts +14 -0
- package/src/lib/fields/file/file.spec.ts +134 -0
- package/src/lib/fields/file/file.ts +281 -0
- package/src/lib/fields/group/group.descriptor.ts +21 -0
- package/src/lib/fields/group/group.ts +110 -0
- package/src/lib/fields/image/image.descriptor.ts +12 -0
- package/src/lib/fields/image/image.spec.ts +117 -0
- package/src/lib/fields/image/image.ts +281 -0
- package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
- package/src/lib/fields/key-value/key-value.ts +283 -0
- package/src/lib/fields/list/list.descriptor.ts +21 -0
- package/src/lib/fields/list/list.ts +376 -0
- package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
- package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
- package/src/lib/fields/multiselect/multiselect.ts +152 -0
- package/src/lib/fields/number/number.descriptor.ts +12 -0
- package/src/lib/fields/number/number.ts +40 -0
- package/src/lib/fields/phone/phone.descriptor.ts +12 -0
- package/src/lib/fields/phone/phone.ts +46 -0
- package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
- package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
- package/src/lib/fields/relation/relation.descriptor.ts +12 -0
- package/src/lib/fields/relation/relation.spec.ts +193 -0
- package/src/lib/fields/relation/relation.ts +224 -0
- package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
- package/src/lib/fields/rich-select/rich-select.ts +175 -0
- package/src/lib/fields/select/radio-mode.ts +25 -0
- package/src/lib/fields/select/select.descriptor.ts +12 -0
- package/src/lib/fields/select/select.spec.ts +162 -0
- package/src/lib/fields/select/select.ts +210 -0
- package/src/lib/fields/slug/slug.descriptor.ts +19 -0
- package/src/lib/fields/slug/slug.ts +67 -0
- package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
- package/src/lib/fields/string-list/string-list.ts +222 -0
- package/src/lib/fields/text/text.descriptor.ts +12 -0
- package/src/lib/fields/text/text.ts +35 -0
- package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
- package/src/lib/fields/textarea/textarea.spec.ts +117 -0
- package/src/lib/fields/textarea/textarea.ts +126 -0
- package/src/lib/fields/time/time.descriptor.ts +12 -0
- package/src/lib/fields/time/time.ts +28 -0
- package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
- package/src/lib/fields/url/url.descriptor.ts +12 -0
- package/src/lib/fields/url/url.ts +53 -0
- package/src/lib/gas-admin-ui.spec.ts +21 -0
- package/src/lib/gas-admin-ui.ts +9 -0
- package/src/lib/layouts/descriptors.ts +9 -0
- package/src/lib/pages/dashboard/dashboard.ts +167 -0
- package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
- package/src/lib/pages/detail-view/detail-view.ts +570 -0
- package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
- package/src/lib/pages/form-view/form-model.ts +88 -0
- package/src/lib/pages/form-view/form-schema.ts +255 -0
- package/src/lib/pages/form-view/form-view.ts +1622 -0
- package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
- package/src/lib/pages/list-view/list-view.ts +882 -0
- package/src/lib/pages/login/login.spec.ts +109 -0
- package/src/lib/pages/login/login.ts +227 -0
- package/src/lib/pages/resource-page/resource-page.ts +234 -0
- package/src/lib/shared/components/badge/badge.ts +35 -0
- package/src/lib/shared/components/button/button.ts +65 -0
- package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
- package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
- package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
- package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
- package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
- package/src/lib/shared/components/drawer/drawer.ts +172 -0
- package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
- package/src/lib/shared/components/modal/modal.spec.ts +149 -0
- package/src/lib/shared/components/modal/modal.ts +145 -0
- package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
- package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
- package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
- package/src/lib/shared/components/toast/toast.ts +58 -0
- package/src/lib/shared/utils/col-span.ts +25 -0
- package/src/lib/shared/utils/condition-evaluator.ts +53 -0
- package/src/lib/shared/utils/depends-on.ts +48 -0
- package/src/lib/shared/utils/dialog-focus.ts +28 -0
- package/src/lib/shared/utils/field-path.ts +49 -0
- package/src/lib/shared/utils/gap-sorting.ts +144 -0
- package/src/lib/shared/utils/seed.spec.ts +64 -0
- package/src/lib/shared/utils/seed.ts +149 -0
- package/src/lib/shared/utils/slugify.ts +13 -0
- package/src/lib/shared/utils/validators.ts +41 -0
- package/src/lib/shell/layout/layout.ts +63 -0
- package/src/lib/shell/shell.routes.spec.ts +59 -0
- package/src/lib/shell/shell.routes.ts +122 -0
- package/src/lib/shell/sidebar/sidebar.ts +139 -0
- package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
- package/src/lib/views/calendar-view/calendar-model.ts +426 -0
- package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
- package/src/lib/views/calendar-view/calendar-view.ts +568 -0
- package/src/lib/views/composer/composer-host.spec.ts +301 -0
- package/src/lib/views/composer/composer-host.ts +248 -0
- package/src/lib/views/composer/composer-imports.spec.ts +54 -0
- package/src/lib/views/composer/view-context.spec.ts +117 -0
- package/src/lib/views/composer/view-context.ts +109 -0
- package/src/lib/views/composer/view-slot.spec.ts +130 -0
- package/src/lib/views/composer/view-slot.ts +178 -0
- package/src/lib/views/descriptors.ts +46 -0
- package/src/lib/views/kanban-view/kanban-model.ts +173 -0
- package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
- package/src/lib/views/kanban-view/kanban-view.ts +327 -0
- package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
- package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
- package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
- package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
- package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
- package/src/lib/widgets/chart-line/chart-line.ts +219 -0
- package/src/lib/widgets/descriptors.ts +29 -0
- package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
- package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
- package/src/lib/widgets/metric-card/metric-card.ts +48 -0
- package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
- package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
- package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
- package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
- package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
- package/src/lib/widgets/record-list/record-list.ts +115 -0
- package/src/lib/widgets/series.spec.ts +42 -0
- package/src/lib/widgets/series.ts +56 -0
- package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
- package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
- package/src/lib/widgets/status-progress/status-progress.ts +104 -0
- package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
- package/src/lib/widgets/widget-host/widget-host.ts +99 -0
- package/src/public-api.ts +104 -0
- package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +10 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
- package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
- package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
- package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
|
@@ -0,0 +1,1142 @@
|
|
|
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
|
+
});
|