@nocobase/flow-engine 2.0.0-alpha.2
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/LICENSE +661 -0
- package/README.md +30 -0
- package/lib/ContextPathProxy.d.ts +17 -0
- package/lib/ContextPathProxy.js +65 -0
- package/lib/ElementProxy.d.ts +17 -0
- package/lib/ElementProxy.js +93 -0
- package/lib/FlowContextProvider.d.ts +24 -0
- package/lib/FlowContextProvider.js +82 -0
- package/lib/FlowDefinition.d.ts +423 -0
- package/lib/FlowDefinition.js +257 -0
- package/lib/JSRunner.d.ts +32 -0
- package/lib/JSRunner.js +95 -0
- package/lib/ReactView.d.ts +20 -0
- package/lib/ReactView.js +120 -0
- package/lib/ViewScopedFlowEngine.d.ts +23 -0
- package/lib/ViewScopedFlowEngine.js +81 -0
- package/lib/acl/Acl.d.ts +31 -0
- package/lib/acl/Acl.js +115 -0
- package/lib/action-registry/BaseActionRegistry.d.ts +23 -0
- package/lib/action-registry/BaseActionRegistry.js +57 -0
- package/lib/action-registry/EngineActionRegistry.d.ts +20 -0
- package/lib/action-registry/EngineActionRegistry.js +47 -0
- package/lib/action-registry/ModelActionRegistry.d.ts +34 -0
- package/lib/action-registry/ModelActionRegistry.js +79 -0
- package/lib/components/DynamicFlowsEditor.d.ts +17 -0
- package/lib/components/DynamicFlowsEditor.js +49 -0
- package/lib/components/FieldModelRenderer.d.ts +10 -0
- package/lib/components/FieldModelRenderer.js +94 -0
- package/lib/components/FlowContextSelector.d.ts +11 -0
- package/lib/components/FlowContextSelector.js +221 -0
- package/lib/components/FlowErrorFallback.d.ts +25 -0
- package/lib/components/FlowErrorFallback.js +264 -0
- package/lib/components/FlowModelRenderer.d.ts +64 -0
- package/lib/components/FlowModelRenderer.js +254 -0
- package/lib/components/FormItem.d.ts +18 -0
- package/lib/components/FormItem.js +147 -0
- package/lib/components/common/FlowSettingsButton.d.ts +11 -0
- package/lib/components/common/FlowSettingsButton.js +66 -0
- package/lib/components/common/index.d.ts +9 -0
- package/lib/components/common/index.js +30 -0
- package/lib/components/common/withFlowDesignMode.d.ts +26 -0
- package/lib/components/common/withFlowDesignMode.js +61 -0
- package/lib/components/dnd/getMousePositionOnElement.d.ts +50 -0
- package/lib/components/dnd/getMousePositionOnElement.js +95 -0
- package/lib/components/dnd/index.d.ts +24 -0
- package/lib/components/dnd/index.js +164 -0
- package/lib/components/dnd/moveBlock.d.ts +33 -0
- package/lib/components/dnd/moveBlock.js +302 -0
- package/lib/components/index.d.ts +18 -0
- package/lib/components/index.js +48 -0
- package/lib/components/settings/independents/dropdown/FlowsDropdownButton.d.ts +46 -0
- package/lib/components/settings/independents/dropdown/FlowsDropdownButton.js +225 -0
- package/lib/components/settings/independents/dropdown/index.d.ts +9 -0
- package/lib/components/settings/independents/dropdown/index.js +30 -0
- package/lib/components/settings/independents/index.d.ts +1 -0
- package/lib/components/settings/independents/index.js +30 -0
- package/lib/components/settings/index.d.ts +10 -0
- package/lib/components/settings/index.js +32 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +24 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +501 -0
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.d.ts +45 -0
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +231 -0
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +111 -0
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +484 -0
- package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.d.ts +26 -0
- package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.js +342 -0
- package/lib/components/settings/wrappers/contextual/StepSettings.d.ts +23 -0
- package/lib/components/settings/wrappers/contextual/StepSettings.js +110 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.d.ts +20 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +206 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.d.ts +20 -0
- package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.js +47 -0
- package/lib/components/settings/wrappers/contextual/index.d.ts +14 -0
- package/lib/components/settings/wrappers/contextual/index.js +40 -0
- package/lib/components/settings/wrappers/embedded/FlowSettings.d.ts +33 -0
- package/lib/components/settings/wrappers/embedded/FlowSettings.js +207 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettings.d.ts +41 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettings.js +84 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.d.ts +16 -0
- package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.js +88 -0
- package/lib/components/settings/wrappers/embedded/index.d.ts +10 -0
- package/lib/components/settings/wrappers/embedded/index.js +32 -0
- package/lib/components/settings/wrappers/index.d.ts +2 -0
- package/lib/components/settings/wrappers/index.js +32 -0
- package/lib/components/subModel/AddSubModelButton.d.ts +62 -0
- package/lib/components/subModel/AddSubModelButton.js +415 -0
- package/lib/components/subModel/LazyDropdown.d.ts +55 -0
- package/lib/components/subModel/LazyDropdown.js +524 -0
- package/lib/components/subModel/index.d.ts +10 -0
- package/lib/components/subModel/index.js +32 -0
- package/lib/components/subModel/utils.d.ts +34 -0
- package/lib/components/subModel/utils.js +287 -0
- package/lib/components/variables/InlineVariableTag.d.ts +21 -0
- package/lib/components/variables/InlineVariableTag.js +123 -0
- package/lib/components/variables/SlateVariableEditor.d.ts +46 -0
- package/lib/components/variables/SlateVariableEditor.js +302 -0
- package/lib/components/variables/VariableInput.d.ts +11 -0
- package/lib/components/variables/VariableInput.js +322 -0
- package/lib/components/variables/VariableTag.d.ts +11 -0
- package/lib/components/variables/VariableTag.js +148 -0
- package/lib/components/variables/VariableTrigger.d.ts +20 -0
- package/lib/components/variables/VariableTrigger.js +136 -0
- package/lib/components/variables/index.d.ts +15 -0
- package/lib/components/variables/index.js +53 -0
- package/lib/components/variables/types.d.ts +93 -0
- package/lib/components/variables/types.js +24 -0
- package/lib/components/variables/useResolvedMetaTree.d.ts +19 -0
- package/lib/components/variables/useResolvedMetaTree.js +91 -0
- package/lib/components/variables/utils.d.ts +22 -0
- package/lib/components/variables/utils.js +177 -0
- package/lib/data-source/index.d.ts +180 -0
- package/lib/data-source/index.js +733 -0
- package/lib/data-source/jioToJoiSchema.d.ts +19 -0
- package/lib/data-source/jioToJoiSchema.js +114 -0
- package/lib/decorators/index.d.ts +9 -0
- package/lib/decorators/index.js +36 -0
- package/lib/decorators/largeField.d.ts +9 -0
- package/lib/decorators/largeField.js +42 -0
- package/lib/emitter.d.ts +16 -0
- package/lib/emitter.js +58 -0
- package/lib/event-registry/BaseEventRegistry.d.ts +22 -0
- package/lib/event-registry/BaseEventRegistry.js +57 -0
- package/lib/event-registry/EngineEventRegistry.d.ts +19 -0
- package/lib/event-registry/EngineEventRegistry.js +47 -0
- package/lib/event-registry/ModelEventRegistry.d.ts +33 -0
- package/lib/event-registry/ModelEventRegistry.js +79 -0
- package/lib/executor/FlowExecutor.d.ts +26 -0
- package/lib/executor/FlowExecutor.js +262 -0
- package/lib/flow-registry/BaseFlowRegistry.d.ts +46 -0
- package/lib/flow-registry/BaseFlowRegistry.js +86 -0
- package/lib/flow-registry/GlobalFlowRegistry.d.ts +22 -0
- package/lib/flow-registry/GlobalFlowRegistry.js +95 -0
- package/lib/flow-registry/InstanceFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/InstanceFlowRegistry.js +59 -0
- package/lib/flow-registry/index.d.ts +11 -0
- package/lib/flow-registry/index.js +34 -0
- package/lib/flowContext.d.ts +215 -0
- package/lib/flowContext.js +1266 -0
- package/lib/flowEngine.d.ts +340 -0
- package/lib/flowEngine.js +781 -0
- package/lib/flowI18n.d.ts +46 -0
- package/lib/flowI18n.js +117 -0
- package/lib/flowSettings.d.ts +266 -0
- package/lib/flowSettings.js +850 -0
- package/lib/hooks/index.d.ts +14 -0
- package/lib/hooks/index.js +40 -0
- package/lib/hooks/useApplyAutoFlows.d.ts +21 -0
- package/lib/hooks/useApplyAutoFlows.js +62 -0
- package/lib/hooks/useFlowModel.d.ts +29 -0
- package/lib/hooks/useFlowModel.js +72 -0
- package/lib/hooks/useFlowModelById.d.ts +11 -0
- package/lib/hooks/useFlowModelById.js +61 -0
- package/lib/hooks/useFlowSettingsContext.d.ts +20 -0
- package/lib/hooks/useFlowSettingsContext.js +61 -0
- package/lib/hooks/useFlowStep.d.ts +17 -0
- package/lib/hooks/useFlowStep.js +56 -0
- package/lib/hooks/useNiceDropdownMaxHeight.d.ts +13 -0
- package/lib/hooks/useNiceDropdownMaxHeight.js +52 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +73 -0
- package/lib/locale/en-US.json +61 -0
- package/lib/locale/index.d.ts +141 -0
- package/lib/locale/index.js +70 -0
- package/lib/locale/zh-CN.json +61 -0
- package/lib/models/CollectionFieldModel.d.ts +50 -0
- package/lib/models/CollectionFieldModel.js +242 -0
- package/lib/models/DisplayItemModel.d.ts +12 -0
- package/lib/models/DisplayItemModel.js +41 -0
- package/lib/models/EditableItemModel.d.ts +12 -0
- package/lib/models/EditableItemModel.js +41 -0
- package/lib/models/FilterableItemModel.d.ts +12 -0
- package/lib/models/FilterableItemModel.js +41 -0
- package/lib/models/flowModel.d.ts +344 -0
- package/lib/models/flowModel.js +1133 -0
- package/lib/models/forkFlowModel.d.ts +83 -0
- package/lib/models/forkFlowModel.js +257 -0
- package/lib/models/index.d.ts +14 -0
- package/lib/models/index.js +40 -0
- package/lib/provider.d.ts +22 -0
- package/lib/provider.js +114 -0
- package/lib/resources/apiResource.d.ts +34 -0
- package/lib/resources/apiResource.js +153 -0
- package/lib/resources/baseRecordResource.d.ts +61 -0
- package/lib/resources/baseRecordResource.js +264 -0
- package/lib/resources/filterItem.d.ts +33 -0
- package/lib/resources/filterItem.js +93 -0
- package/lib/resources/flowResource.d.ts +45 -0
- package/lib/resources/flowResource.js +146 -0
- package/lib/resources/index.d.ts +15 -0
- package/lib/resources/index.js +42 -0
- package/lib/resources/multiRecordResource.d.ts +53 -0
- package/lib/resources/multiRecordResource.js +230 -0
- package/lib/resources/singleRecordResource.d.ts +23 -0
- package/lib/resources/singleRecordResource.js +111 -0
- package/lib/resources/sqlResource.d.ts +73 -0
- package/lib/resources/sqlResource.js +294 -0
- package/lib/runjs-context/contexts/FlowRunJSContext.d.ts +38 -0
- package/lib/runjs-context/contexts/FlowRunJSContext.js +217 -0
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +66 -0
- package/lib/runjs-context/contexts/JSBlockRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +78 -0
- package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.d.ts +12 -0
- package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.js +59 -0
- package/lib/runjs-context/contexts/JSFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +70 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +66 -0
- package/lib/runjs-context/contexts/JSRecordActionRunJSContext.d.ts +12 -0
- package/lib/runjs-context/contexts/JSRecordActionRunJSContext.js +61 -0
- package/lib/runjs-context/contexts/LinkageRunJSContext.d.ts +12 -0
- package/lib/runjs-context/contexts/LinkageRunJSContext.js +62 -0
- package/lib/runjs-context/helpers.d.ts +17 -0
- package/lib/runjs-context/helpers.js +79 -0
- package/lib/runjs-context/index.d.ts +19 -0
- package/lib/runjs-context/index.js +57 -0
- package/lib/runjs-context/registry.d.ts +17 -0
- package/lib/runjs-context/registry.js +93 -0
- package/lib/runjs-context/snippets/global/api-request-get.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/api-request-get.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/api-request-post.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/api-request-post.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/console-log-ctx.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/console-log-ctx.snippet.js +41 -0
- package/lib/runjs-context/snippets/global/copy-record-json.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/copy-record-json.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/log-json-record.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/log-json-record.snippet.js +42 -0
- package/lib/runjs-context/snippets/global/message-error.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/message-error.snippet.js +41 -0
- package/lib/runjs-context/snippets/global/message-success.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/message-success.snippet.js +41 -0
- package/lib/runjs-context/snippets/global/notification-open.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/notification-open.snippet.js +43 -0
- package/lib/runjs-context/snippets/global/open-view-dialog.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/open-view-dialog.snippet.js +47 -0
- package/lib/runjs-context/snippets/global/open-view-drawer.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/open-view-drawer.snippet.js +47 -0
- package/lib/runjs-context/snippets/global/requireAsync.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/requireAsync.snippet.js +46 -0
- package/lib/runjs-context/snippets/global/sleep.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/sleep.snippet.js +43 -0
- package/lib/runjs-context/snippets/global/try-catch-async.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/try-catch-async.snippet.js +44 -0
- package/lib/runjs-context/snippets/global/view-navigation-push.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/global/view-navigation-push.snippet.js +44 -0
- package/lib/runjs-context/snippets/global/window-open.snippet.d.ts +16 -0
- package/lib/runjs-context/snippets/global/window-open.snippet.js +41 -0
- package/lib/runjs-context/snippets/index.d.ts +11 -0
- package/lib/runjs-context/snippets/index.js +94 -0
- package/lib/runjs-context/snippets/libs/echarts-init.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/libs/echarts-init.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.js +44 -0
- package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.js +43 -0
- package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.js +43 -0
- package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.js +40 -0
- package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.js +42 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.js +41 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.js +41 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.js +46 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.js +45 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.js +56 -0
- package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.js +42 -0
- package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.js +41 -0
- package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.js +40 -0
- package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.js +44 -0
- package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.js +60 -0
- package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.js +59 -0
- package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.js +60 -0
- package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.d.ts +15 -0
- package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.js +60 -0
- package/lib/runjs-context/snippets/types.d.ts +16 -0
- package/lib/runjs-context/snippets/types.js +24 -0
- package/lib/types.d.ts +398 -0
- package/lib/types.js +43 -0
- package/lib/utils/autoFlowError.d.ts +16 -0
- package/lib/utils/autoFlowError.js +53 -0
- package/lib/utils/constants.d.ts +29 -0
- package/lib/utils/constants.js +77 -0
- package/lib/utils/context.d.ts +40 -0
- package/lib/utils/context.js +63 -0
- package/lib/utils/createCollectionContextMeta.d.ts +11 -0
- package/lib/utils/createCollectionContextMeta.js +117 -0
- package/lib/utils/exceptions.d.ts +22 -0
- package/lib/utils/exceptions.js +62 -0
- package/lib/utils/flow-definitions.d.ts +11 -0
- package/lib/utils/flow-definitions.js +40 -0
- package/lib/utils/index.d.ts +22 -0
- package/lib/utils/index.js +117 -0
- package/lib/utils/inheritance.d.ts +16 -0
- package/lib/utils/inheritance.js +53 -0
- package/lib/utils/params-resolvers.d.ts +51 -0
- package/lib/utils/params-resolvers.js +309 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +34 -0
- package/lib/utils/parsePathnameToViewParams.js +84 -0
- package/lib/utils/safeGlobals.d.ts +16 -0
- package/lib/utils/safeGlobals.js +179 -0
- package/lib/utils/schema-utils.d.ts +40 -0
- package/lib/utils/schema-utils.js +161 -0
- package/lib/utils/serverContextParams.d.ts +28 -0
- package/lib/utils/serverContextParams.js +106 -0
- package/lib/utils/setupRuntimeContextSteps.d.ts +19 -0
- package/lib/utils/setupRuntimeContextSteps.js +88 -0
- package/lib/utils/translation.d.ts +18 -0
- package/lib/utils/translation.js +58 -0
- package/lib/utils/variablesParams.d.ts +51 -0
- package/lib/utils/variablesParams.js +150 -0
- package/lib/views/DialogComponent.d.ts +22 -0
- package/lib/views/DialogComponent.js +98 -0
- package/lib/views/DrawerComponent.d.ts +11 -0
- package/lib/views/DrawerComponent.js +101 -0
- package/lib/views/FlowView.d.ts +76 -0
- package/lib/views/FlowView.js +81 -0
- package/lib/views/PageComponent.d.ts +10 -0
- package/lib/views/PageComponent.js +167 -0
- package/lib/views/ViewNavigation.d.ts +45 -0
- package/lib/views/ViewNavigation.js +97 -0
- package/lib/views/createViewMeta.d.ts +16 -0
- package/lib/views/createViewMeta.js +171 -0
- package/lib/views/index.d.ts +13 -0
- package/lib/views/index.js +48 -0
- package/lib/views/useDialog.d.ts +32 -0
- package/lib/views/useDialog.js +199 -0
- package/lib/views/useDrawer.d.ts +33 -0
- package/lib/views/useDrawer.js +206 -0
- package/lib/views/usePage.d.ts +32 -0
- package/lib/views/usePage.js +193 -0
- package/lib/views/usePatchElement.d.ts +10 -0
- package/lib/views/usePatchElement.js +54 -0
- package/lib/views/usePopover.d.ts +17 -0
- package/lib/views/usePopover.js +159 -0
- package/package.json +37 -0
- package/src/ContextPathProxy.ts +45 -0
- package/src/ElementProxy.ts +69 -0
- package/src/FlowContextProvider.tsx +40 -0
- package/src/FlowDefinition.ts +275 -0
- package/src/JSRunner.ts +84 -0
- package/src/ReactView.tsx +104 -0
- package/src/ViewScopedFlowEngine.ts +75 -0
- package/src/__tests__/ElementProxy.test.ts +51 -0
- package/src/__tests__/JSRunner.test.ts +92 -0
- package/src/__tests__/ReactView.test.tsx +63 -0
- package/src/__tests__/context-path-proxy.test.ts +35 -0
- package/src/__tests__/flow-engine.test.ts +189 -0
- package/src/__tests__/flowContext.test.ts +2012 -0
- package/src/__tests__/flowEngine.saveModel.test.ts +171 -0
- package/src/__tests__/flowI18n.test.ts +28 -0
- package/src/__tests__/flowModel.getFlows.test.ts +61 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +78 -0
- package/src/__tests__/flowRuntimeContext.test.ts +187 -0
- package/src/__tests__/flowSettings.open.test.tsx +1920 -0
- package/src/__tests__/flowSettings.test.ts +566 -0
- package/src/__tests__/globalFlowRegistry.test.ts +77 -0
- package/src/__tests__/isFieldInterfaceMatch.test.ts +51 -0
- package/src/__tests__/metaTreeNodeCache.test.ts +234 -0
- package/src/__tests__/path-aggregation.test.ts +85 -0
- package/src/__tests__/provider.test.tsx +28 -0
- package/src/__tests__/renderHiddenInConfig.test.tsx +91 -0
- package/src/__tests__/runjsContext.test.ts +60 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +212 -0
- package/src/acl/Acl.tsx +109 -0
- package/src/acl/__tests__/Acl.test.tsx +72 -0
- package/src/action-registry/BaseActionRegistry.ts +46 -0
- package/src/action-registry/EngineActionRegistry.ts +32 -0
- package/src/action-registry/ModelActionRegistry.ts +75 -0
- package/src/action-registry/__tests__/engineActionRegistry.test.ts +43 -0
- package/src/action-registry/__tests__/modelActionRegistry.test.ts +107 -0
- package/src/components/DynamicFlowsEditor.tsx +318 -0
- package/src/components/FieldModelRenderer.tsx +62 -0
- package/src/components/FlowContextSelector.tsx +255 -0
- package/src/components/FlowErrorFallback.tsx +316 -0
- package/src/components/FlowModelRenderer.tsx +428 -0
- package/src/components/FormItem.tsx +130 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +226 -0
- package/src/components/common/FlowSettingsButton.tsx +30 -0
- package/src/components/common/index.ts +10 -0
- package/src/components/common/withFlowDesignMode.tsx +49 -0
- package/src/components/dnd/getMousePositionOnElement.ts +115 -0
- package/src/components/dnd/index.tsx +128 -0
- package/src/components/dnd/moveBlock.ts +379 -0
- package/src/components/index.ts +20 -0
- package/src/components/settings/independents/dropdown/FlowsDropdownButton.tsx +279 -0
- package/src/components/settings/independents/dropdown/index.ts +10 -0
- package/src/components/settings/independents/index.ts +2 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +617 -0
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +292 -0
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +655 -0
- package/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx +446 -0
- package/src/components/settings/wrappers/contextual/StepSettings.tsx +109 -0
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +217 -0
- package/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx +32 -0
- package/src/components/settings/wrappers/contextual/index.ts +15 -0
- package/src/components/settings/wrappers/embedded/FlowSettings.tsx +258 -0
- package/src/components/settings/wrappers/embedded/FlowsSettings.tsx +111 -0
- package/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx +96 -0
- package/src/components/settings/wrappers/embedded/index.ts +11 -0
- package/src/components/settings/wrappers/index.ts +5 -0
- package/src/components/subModel/AddSubModelButton.tsx +575 -0
- package/src/components/subModel/LazyDropdown.tsx +714 -0
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +1185 -0
- package/src/components/subModel/__tests__/buildWrapperFieldChildren.test.ts +192 -0
- package/src/components/subModel/__tests__/utils.test.ts +425 -0
- package/src/components/subModel/index.ts +12 -0
- package/src/components/subModel/utils.ts +278 -0
- package/src/components/variables/InlineVariableTag.tsx +97 -0
- package/src/components/variables/SlateVariableEditor.tsx +384 -0
- package/src/components/variables/VariableInput.tsx +342 -0
- package/src/components/variables/VariableTag.tsx +123 -0
- package/src/components/variables/VariableTrigger.tsx +116 -0
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +553 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +550 -0
- package/src/components/variables/__tests__/VariableTag.test.tsx +347 -0
- package/src/components/variables/__tests__/test-utils.tsx +62 -0
- package/src/components/variables/__tests__/utils.test.ts +310 -0
- package/src/components/variables/index.ts +16 -0
- package/src/components/variables/types.ts +100 -0
- package/src/components/variables/useResolvedMetaTree.ts +76 -0
- package/src/components/variables/utils.ts +192 -0
- package/src/data-source/__tests__/collection.test.ts +58 -0
- package/src/data-source/__tests__/index.test.ts +82 -0
- package/src/data-source/__tests__/jioToJoiSchema.test.ts +56 -0
- package/src/data-source/index.ts +812 -0
- package/src/data-source/jioToJoiSchema.ts +103 -0
- package/src/decorators/index.ts +10 -0
- package/src/decorators/largeField.ts +14 -0
- package/src/emitter.ts +33 -0
- package/src/event-registry/BaseEventRegistry.ts +40 -0
- package/src/event-registry/EngineEventRegistry.ts +26 -0
- package/src/event-registry/ModelEventRegistry.ts +69 -0
- package/src/event-registry/__tests__/engineEventRegistry.test.ts +48 -0
- package/src/executor/FlowExecutor.ts +256 -0
- package/src/executor/__tests__/eventStep.test.ts +157 -0
- package/src/executor/__tests__/flowExecutor.test.ts +163 -0
- package/src/flow-registry/BaseFlowRegistry.ts +91 -0
- package/src/flow-registry/GlobalFlowRegistry.ts +82 -0
- package/src/flow-registry/InstanceFlowRegistry.ts +39 -0
- package/src/flow-registry/__tests__/globalFlowRegistry.test.ts +141 -0
- package/src/flow-registry/__tests__/instance-and-global-registry.test.ts +67 -0
- package/src/flow-registry/__tests__/instanceFlowRegistry.test.ts +83 -0
- package/src/flow-registry/index.ts +12 -0
- package/src/flowContext.ts +1639 -0
- package/src/flowEngine.ts +905 -0
- package/src/flowI18n.ts +96 -0
- package/src/flowSettings.ts +1045 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useApplyAutoFlows.ts +51 -0
- package/src/hooks/useFlowModel.tsx +59 -0
- package/src/hooks/useFlowModelById.ts +37 -0
- package/src/hooks/useFlowSettingsContext.tsx +37 -0
- package/src/hooks/useFlowStep.tsx +19 -0
- package/src/hooks/useNiceDropdownMaxHeight.ts +34 -0
- package/src/index.ts +38 -0
- package/src/locale/en-US.json +61 -0
- package/src/locale/index.ts +38 -0
- package/src/locale/zh-CN.json +61 -0
- package/src/models/CollectionFieldModel.tsx +269 -0
- package/src/models/DisplayItemModel.tsx +13 -0
- package/src/models/EditableItemModel.tsx +13 -0
- package/src/models/FilterableItemModel.tsx +13 -0
- package/src/models/__tests__/CollectionFieldModel.test.ts +122 -0
- package/src/models/__tests__/defaultParams-on-create.test.ts +83 -0
- package/src/models/__tests__/flow-model-oninit.test.ts +44 -0
- package/src/models/__tests__/flowModel.actions.integration.test.ts +100 -0
- package/src/models/__tests__/flowModel.getFlows.sort.test.ts +100 -0
- package/src/models/__tests__/flowModel.test.ts +2746 -0
- package/src/models/__tests__/flowRegistry.test.ts +512 -0
- package/src/models/__tests__/forkFlowModel.test.ts +1047 -0
- package/src/models/__tests__/model-actions.test.ts +70 -0
- package/src/models/__tests__/model-events.test.ts +69 -0
- package/src/models/flowModel.tsx +1398 -0
- package/src/models/forkFlowModel.ts +287 -0
- package/src/models/index.ts +17 -0
- package/src/provider.tsx +101 -0
- package/src/resources/__tests__/apiResource.test.ts +201 -0
- package/src/resources/__tests__/baseRecordResource.test.ts +262 -0
- package/src/resources/__tests__/filterItem.test.ts +260 -0
- package/src/resources/__tests__/flowResource.test.ts +127 -0
- package/src/resources/apiResource.ts +148 -0
- package/src/resources/baseRecordResource.ts +279 -0
- package/src/resources/filterItem.ts +74 -0
- package/src/resources/flowResource.ts +143 -0
- package/src/resources/index.ts +17 -0
- package/src/resources/multiRecordResource.ts +219 -0
- package/src/resources/singleRecordResource.ts +83 -0
- package/src/resources/sqlResource.ts +299 -0
- package/src/runjs-context/contexts/FlowRunJSContext.ts +190 -0
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +39 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +52 -0
- package/src/runjs-context/contexts/JSCollectionActionRunJSContext.ts +32 -0
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +43 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +39 -0
- package/src/runjs-context/contexts/JSRecordActionRunJSContext.ts +34 -0
- package/src/runjs-context/contexts/LinkageRunJSContext.ts +35 -0
- package/src/runjs-context/helpers.ts +56 -0
- package/src/runjs-context/index.ts +20 -0
- package/src/runjs-context/registry.ts +65 -0
- package/src/runjs-context/snippets/global/api-request-get.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/api-request-post.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/console-log-ctx.snippet.ts +19 -0
- package/src/runjs-context/snippets/global/copy-record-json.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/copy-to-clipboard.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/log-json-record.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/message-error.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/message-success.snippet.ts +20 -0
- package/src/runjs-context/snippets/global/notification-open.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/open-view-dialog.snippet.ts +26 -0
- package/src/runjs-context/snippets/global/open-view-drawer.snippet.ts +26 -0
- package/src/runjs-context/snippets/global/requireAsync.snippet.ts +24 -0
- package/src/runjs-context/snippets/global/sleep.snippet.ts +21 -0
- package/src/runjs-context/snippets/global/try-catch-async.snippet.ts +22 -0
- package/src/runjs-context/snippets/global/view-navigation-push.snippet.ts +23 -0
- package/src/runjs-context/snippets/global/window-open.snippet.ts +19 -0
- package/src/runjs-context/snippets/index.ts +59 -0
- package/src/runjs-context/snippets/libs/echarts-init.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/actions/collection-selected-count.snippet.ts +22 -0
- package/src/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.ts +21 -0
- package/src/runjs-context/snippets/scene/actions/record-id-message.snippet.ts +21 -0
- package/src/runjs-context/snippets/scene/actions/run-action-basic.snippet.ts +18 -0
- package/src/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.ts +29 -0
- package/src/runjs-context/snippets/scene/jsblock/append-style.snippet.ts +20 -0
- package/src/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.ts +19 -0
- package/src/runjs-context/snippets/scene/jsblock/render-basic.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.ts +24 -0
- package/src/runjs-context/snippets/scene/jsblock/render-card.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/jsblock/render-react.snippet.ts +34 -0
- package/src/runjs-context/snippets/scene/jsfield/color-by-value.snippet.ts +20 -0
- package/src/runjs-context/snippets/scene/jsfield/format-number.snippet.ts +19 -0
- package/src/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.ts +18 -0
- package/src/runjs-context/snippets/scene/jsitem/render-basic.snippet.ts +27 -0
- package/src/runjs-context/snippets/scene/linkage/set-disabled.snippet.ts +38 -0
- package/src/runjs-context/snippets/scene/linkage/set-field-value.snippet.ts +37 -0
- package/src/runjs-context/snippets/scene/linkage/set-required.snippet.ts +38 -0
- package/src/runjs-context/snippets/scene/linkage/toggle-visible.snippet.ts +38 -0
- package/src/runjs-context/snippets/types.ts +17 -0
- package/src/types.ts +474 -0
- package/src/utils/__tests__/context.test.ts +93 -0
- package/src/utils/__tests__/params-resolvers.test.ts +652 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +104 -0
- package/src/utils/__tests__/safeGlobals.test.ts +29 -0
- package/src/utils/__tests__/utils.test.ts +1021 -0
- package/src/utils/__tests__/variablesParams.test.ts +52 -0
- package/src/utils/autoFlowError.ts +29 -0
- package/src/utils/constants.ts +60 -0
- package/src/utils/context.ts +70 -0
- package/src/utils/createCollectionContextMeta.ts +122 -0
- package/src/utils/exceptions.ts +36 -0
- package/src/utils/flow-definitions.ts +16 -0
- package/src/utils/index.ts +63 -0
- package/src/utils/inheritance.ts +39 -0
- package/src/utils/params-resolvers.ts +482 -0
- package/src/utils/parsePathnameToViewParams.ts +103 -0
- package/src/utils/safeGlobals.ts +188 -0
- package/src/utils/schema-utils.ts +201 -0
- package/src/utils/serverContextParams.ts +111 -0
- package/src/utils/setupRuntimeContextSteps.ts +89 -0
- package/src/utils/translation.ts +37 -0
- package/src/utils/variablesParams.ts +175 -0
- package/src/views/DialogComponent.tsx +79 -0
- package/src/views/DrawerComponent.tsx +72 -0
- package/src/views/FlowView.tsx +103 -0
- package/src/views/PageComponent.tsx +150 -0
- package/src/views/ViewNavigation.ts +122 -0
- package/src/views/__tests__/FlowView.test.ts +31 -0
- package/src/views/__tests__/ViewNavigation.test.ts +191 -0
- package/src/views/__tests__/usePatchElement.test.tsx +28 -0
- package/src/views/createViewMeta.ts +157 -0
- package/src/views/index.tsx +14 -0
- package/src/views/useDialog.tsx +192 -0
- package/src/views/useDrawer.tsx +205 -0
- package/src/views/usePage.tsx +182 -0
- package/src/views/usePatchElement.tsx +27 -0
- package/src/views/usePopover.tsx +131 -0
|
@@ -0,0 +1,2012 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { FlowContext, FlowRuntimeContext } from '../flowContext';
|
|
12
|
+
import { FlowEngine } from '../flowEngine';
|
|
13
|
+
import { FlowModel } from '../models/flowModel';
|
|
14
|
+
|
|
15
|
+
describe('FlowContext properties and methods', () => {
|
|
16
|
+
it('should return static property value', () => {
|
|
17
|
+
const ctx = new FlowContext();
|
|
18
|
+
ctx.defineProperty('foo', { value: 123 });
|
|
19
|
+
expect(ctx.foo).toBe(123);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return sync value from get', () => {
|
|
23
|
+
const ctx = new FlowContext();
|
|
24
|
+
ctx.defineProperty('bar', { get: () => 456 });
|
|
25
|
+
expect(ctx.bar).toBe(456);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return async value from get', async () => {
|
|
29
|
+
const ctx = new FlowContext();
|
|
30
|
+
ctx.defineProperty('baz', { get: async () => 'hello' });
|
|
31
|
+
expect(await ctx.baz).toBe('hello');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should support context reference in get', () => {
|
|
35
|
+
const ctx = new FlowContext();
|
|
36
|
+
ctx.defineProperty('a', { get: () => 'a' });
|
|
37
|
+
ctx.defineProperty('b', { get: (ctx) => ctx.a + 'b' });
|
|
38
|
+
expect(ctx.b).toBe('ab');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should support context reference in get', () => {
|
|
42
|
+
const ctx1 = new FlowContext();
|
|
43
|
+
ctx1.defineProperty('a', { get: () => 'a' });
|
|
44
|
+
const ctx = new FlowContext();
|
|
45
|
+
ctx.addDelegate(ctx1);
|
|
46
|
+
ctx.defineProperty('b', { get: () => ctx.a + 'b' });
|
|
47
|
+
expect(ctx.b).toBe('ab');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should pass correct context instance to getter function in delegate chain', () => {
|
|
51
|
+
class FlowContext1 extends FlowContext {}
|
|
52
|
+
class FlowContext2 extends FlowContext {}
|
|
53
|
+
const ctx1 = new FlowContext1();
|
|
54
|
+
ctx1.defineProperty('a', { cache: false, get: (ctx) => ctx });
|
|
55
|
+
const ctx2 = new FlowContext2();
|
|
56
|
+
ctx2.addDelegate(ctx1);
|
|
57
|
+
expect(ctx1.a).toBe(ctx1);
|
|
58
|
+
expect(ctx2.a).toBe(ctx2);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should support async context reference in get', async () => {
|
|
62
|
+
const ctx = new FlowContext();
|
|
63
|
+
ctx.defineProperty('c', { get: async () => 'c' });
|
|
64
|
+
ctx.defineProperty('d', { get: async (ctx) => (await ctx.c) + 'd' });
|
|
65
|
+
expect(await ctx.d).toBe('cd');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should queue and reuse promise for concurrent async get calls', async () => {
|
|
69
|
+
const ctx = new FlowContext();
|
|
70
|
+
const getter = vi.fn(async () => {
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
72
|
+
return 'async-value';
|
|
73
|
+
});
|
|
74
|
+
ctx.defineProperty('concurrent', { get: getter });
|
|
75
|
+
|
|
76
|
+
// 并发调用
|
|
77
|
+
const [v1, v2, v3] = await Promise.all([ctx.concurrent, ctx.concurrent, ctx.concurrent]);
|
|
78
|
+
expect(v1).toBe('async-value');
|
|
79
|
+
expect(v2).toBe('async-value');
|
|
80
|
+
expect(v3).toBe('async-value');
|
|
81
|
+
// 只会调用一次 getter
|
|
82
|
+
expect(getter).toHaveBeenCalledTimes(1);
|
|
83
|
+
|
|
84
|
+
// 再次调用,因已缓存,不会再调用 getter
|
|
85
|
+
const v4 = await ctx.concurrent;
|
|
86
|
+
expect(v4).toBe('async-value');
|
|
87
|
+
expect(getter).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should cache get result by default', async () => {
|
|
91
|
+
const ctx = new FlowContext();
|
|
92
|
+
const getter = vi.fn().mockResolvedValue(789);
|
|
93
|
+
ctx.defineProperty('cached', { get: getter });
|
|
94
|
+
|
|
95
|
+
await ctx.cached;
|
|
96
|
+
await ctx.cached;
|
|
97
|
+
expect(getter).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should not cache get result when cache=false', async () => {
|
|
101
|
+
const ctx = new FlowContext();
|
|
102
|
+
const getter = vi.fn().mockResolvedValue(101112);
|
|
103
|
+
ctx.defineProperty('noCache', { get: getter, cache: false });
|
|
104
|
+
|
|
105
|
+
await ctx.noCache;
|
|
106
|
+
await ctx.noCache;
|
|
107
|
+
expect(getter).toHaveBeenCalledTimes(2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should remove cache and pending for property', async () => {
|
|
111
|
+
const ctx = new FlowContext();
|
|
112
|
+
const getter = vi.fn().mockResolvedValue('cached-value');
|
|
113
|
+
ctx.defineProperty('foo', { get: getter });
|
|
114
|
+
|
|
115
|
+
// 首次获取,触发缓存
|
|
116
|
+
await ctx.foo;
|
|
117
|
+
expect(getter).toHaveBeenCalledTimes(1);
|
|
118
|
+
|
|
119
|
+
// 再次获取,命中缓存
|
|
120
|
+
await ctx.foo;
|
|
121
|
+
expect(getter).toHaveBeenCalledTimes(1);
|
|
122
|
+
|
|
123
|
+
// 清除缓存
|
|
124
|
+
ctx.removeCache('foo');
|
|
125
|
+
|
|
126
|
+
// 再次获取,应重新调用 getter
|
|
127
|
+
await ctx.foo;
|
|
128
|
+
expect(getter).toHaveBeenCalledTimes(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should remove cache and pending recursively in delegates', async () => {
|
|
132
|
+
const delegate = new FlowContext();
|
|
133
|
+
const getter = vi.fn().mockResolvedValue('delegate-value');
|
|
134
|
+
delegate.defineProperty('bar', { get: getter });
|
|
135
|
+
|
|
136
|
+
const ctx = new FlowContext();
|
|
137
|
+
ctx.addDelegate(delegate);
|
|
138
|
+
|
|
139
|
+
// 首次获取,触发缓存
|
|
140
|
+
await ctx.bar;
|
|
141
|
+
expect(getter).toHaveBeenCalledTimes(1);
|
|
142
|
+
|
|
143
|
+
// 清除缓存(应递归到 delegate)
|
|
144
|
+
ctx.removeCache('bar');
|
|
145
|
+
|
|
146
|
+
// 再次获取,应重新调用 getter
|
|
147
|
+
await ctx.bar;
|
|
148
|
+
expect(getter).toHaveBeenCalledTimes(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should support delegate context property', () => {
|
|
152
|
+
const delegate = new FlowContext();
|
|
153
|
+
delegate.defineProperty('shared', { value: 'from delegate' });
|
|
154
|
+
|
|
155
|
+
const ctx = new FlowContext();
|
|
156
|
+
ctx.addDelegate(delegate);
|
|
157
|
+
expect(ctx.shared).toBe('from delegate');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should throw sync error in get', () => {
|
|
161
|
+
const ctx = new FlowContext();
|
|
162
|
+
ctx.defineProperty('error', {
|
|
163
|
+
get: () => {
|
|
164
|
+
throw new Error('Oops');
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
expect(() => ctx.error).toThrow('Oops');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should throw async error in get', async () => {
|
|
171
|
+
const ctx = new FlowContext();
|
|
172
|
+
ctx.defineProperty('errorAsync', {
|
|
173
|
+
get: async () => {
|
|
174
|
+
throw new Error('Async Oops');
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
await expect(ctx.errorAsync).rejects.toThrow('Async Oops');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should find property in multi-level delegate chain', () => {
|
|
181
|
+
const root = new FlowContext();
|
|
182
|
+
root.defineProperty('deep', { value: 42 });
|
|
183
|
+
|
|
184
|
+
const mid = new FlowContext();
|
|
185
|
+
mid.addDelegate(root);
|
|
186
|
+
|
|
187
|
+
const ctx = new FlowContext();
|
|
188
|
+
ctx.addDelegate(mid);
|
|
189
|
+
|
|
190
|
+
expect(ctx.deep).toBe(42);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should allow local property to override delegate property', () => {
|
|
194
|
+
const delegate = new FlowContext();
|
|
195
|
+
delegate.defineProperty('foo', { value: 'delegate' });
|
|
196
|
+
|
|
197
|
+
const ctx = new FlowContext();
|
|
198
|
+
ctx.addDelegate(delegate);
|
|
199
|
+
ctx.defineProperty('foo', { value: 'local' });
|
|
200
|
+
|
|
201
|
+
expect(ctx.foo).toBe('local');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return undefined for undefined property', () => {
|
|
205
|
+
const ctx = new FlowContext();
|
|
206
|
+
expect(ctx.nonExistent).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should override property when redefined', () => {
|
|
210
|
+
const ctx = new FlowContext();
|
|
211
|
+
ctx.defineProperty('dup', { value: 1 });
|
|
212
|
+
ctx.defineProperty('dup', { value: 2 });
|
|
213
|
+
|
|
214
|
+
expect(ctx.dup).toBe(2);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should support flat and nested meta in getPropertyMetaTree', () => {
|
|
218
|
+
const ctx = new FlowContext();
|
|
219
|
+
ctx.defineProperty('foo', {
|
|
220
|
+
meta: { type: 'string', title: 'Foo' },
|
|
221
|
+
});
|
|
222
|
+
ctx.defineProperty('bar', {
|
|
223
|
+
meta: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
title: 'Bar',
|
|
226
|
+
properties: {
|
|
227
|
+
baz: { type: 'number', title: 'Baz' },
|
|
228
|
+
qux: { type: 'string', title: 'Qux' },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const tree = ctx.getPropertyMetaTree();
|
|
233
|
+
expect(tree).toEqual([
|
|
234
|
+
{
|
|
235
|
+
name: 'foo',
|
|
236
|
+
title: 'Foo',
|
|
237
|
+
type: 'string',
|
|
238
|
+
interface: undefined,
|
|
239
|
+
uiSchema: undefined,
|
|
240
|
+
paths: ['foo'],
|
|
241
|
+
parentTitles: undefined,
|
|
242
|
+
disabled: false,
|
|
243
|
+
disabledReason: undefined,
|
|
244
|
+
children: undefined,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'bar',
|
|
248
|
+
title: 'Bar',
|
|
249
|
+
type: 'object',
|
|
250
|
+
interface: undefined,
|
|
251
|
+
uiSchema: undefined,
|
|
252
|
+
paths: ['bar'],
|
|
253
|
+
parentTitles: undefined,
|
|
254
|
+
disabled: false,
|
|
255
|
+
disabledReason: undefined,
|
|
256
|
+
children: [
|
|
257
|
+
{
|
|
258
|
+
name: 'baz',
|
|
259
|
+
title: 'Baz',
|
|
260
|
+
type: 'number',
|
|
261
|
+
interface: undefined,
|
|
262
|
+
uiSchema: undefined,
|
|
263
|
+
paths: ['bar', 'baz'],
|
|
264
|
+
parentTitles: ['Bar'],
|
|
265
|
+
disabled: false,
|
|
266
|
+
disabledReason: undefined,
|
|
267
|
+
children: undefined,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'qux',
|
|
271
|
+
title: 'Qux',
|
|
272
|
+
type: 'string',
|
|
273
|
+
interface: undefined,
|
|
274
|
+
uiSchema: undefined,
|
|
275
|
+
paths: ['bar', 'qux'],
|
|
276
|
+
parentTitles: ['Bar'],
|
|
277
|
+
disabled: false,
|
|
278
|
+
disabledReason: undefined,
|
|
279
|
+
children: undefined,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should support delegate meta and local override in getPropertyMetaTree', () => {
|
|
287
|
+
const delegate = new FlowContext();
|
|
288
|
+
delegate.defineProperty('foo', {
|
|
289
|
+
meta: { type: 'string', title: 'Delegate Foo', interface: 'text', uiSchema: { type: 'text' } },
|
|
290
|
+
});
|
|
291
|
+
delegate.defineProperty('bar', {
|
|
292
|
+
meta: { type: 'number', title: 'Delegate Bar', interface: 'number', uiSchema: { type: 'number' } },
|
|
293
|
+
});
|
|
294
|
+
const ctx = new FlowContext();
|
|
295
|
+
ctx.addDelegate(delegate);
|
|
296
|
+
ctx.defineProperty('bar', {
|
|
297
|
+
meta: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
title: 'Local Bar',
|
|
300
|
+
interface: 'object',
|
|
301
|
+
uiSchema: { type: 'object' },
|
|
302
|
+
properties: {
|
|
303
|
+
x: { type: 'string', title: 'X', interface: 'text', uiSchema: { type: 'text' } },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
const tree = ctx.getPropertyMetaTree();
|
|
308
|
+
expect(tree).toEqual([
|
|
309
|
+
{
|
|
310
|
+
name: 'foo',
|
|
311
|
+
title: 'Delegate Foo',
|
|
312
|
+
type: 'string',
|
|
313
|
+
interface: 'text',
|
|
314
|
+
uiSchema: { type: 'text' },
|
|
315
|
+
paths: ['foo'],
|
|
316
|
+
parentTitles: undefined,
|
|
317
|
+
disabled: false,
|
|
318
|
+
disabledReason: undefined,
|
|
319
|
+
children: undefined,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'bar',
|
|
323
|
+
title: 'Local Bar',
|
|
324
|
+
type: 'object',
|
|
325
|
+
interface: 'object',
|
|
326
|
+
uiSchema: { type: 'object' },
|
|
327
|
+
paths: ['bar'],
|
|
328
|
+
parentTitles: undefined,
|
|
329
|
+
disabled: false,
|
|
330
|
+
disabledReason: undefined,
|
|
331
|
+
children: [
|
|
332
|
+
{
|
|
333
|
+
name: 'x',
|
|
334
|
+
title: 'X',
|
|
335
|
+
type: 'string',
|
|
336
|
+
interface: 'text',
|
|
337
|
+
uiSchema: { type: 'text' },
|
|
338
|
+
paths: ['bar', 'x'],
|
|
339
|
+
parentTitles: ['Local Bar'],
|
|
340
|
+
disabled: false,
|
|
341
|
+
disabledReason: undefined,
|
|
342
|
+
children: undefined,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should support single-level mixed sync and async properties in getPropertyMetaTree', async () => {
|
|
350
|
+
const ctx = new FlowContext();
|
|
351
|
+
|
|
352
|
+
// 同步属性
|
|
353
|
+
ctx.defineProperty('syncProp', {
|
|
354
|
+
meta: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
title: 'Sync Property',
|
|
357
|
+
properties: {
|
|
358
|
+
field1: { type: 'string', title: 'Field 1' },
|
|
359
|
+
field2: { type: 'number', title: 'Field 2' },
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// 异步属性
|
|
365
|
+
ctx.defineProperty('asyncProp', {
|
|
366
|
+
meta: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
title: 'Async Property',
|
|
369
|
+
properties: async () => {
|
|
370
|
+
// 模拟异步加载
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
372
|
+
return {
|
|
373
|
+
dynamicField1: { type: 'string', title: 'Dynamic Field 1' },
|
|
374
|
+
dynamicField2: { type: 'boolean', title: 'Dynamic Field 2' },
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const tree = ctx.getPropertyMetaTree();
|
|
381
|
+
|
|
382
|
+
// 检查同步属性
|
|
383
|
+
expect(tree[0]).toEqual({
|
|
384
|
+
name: 'syncProp',
|
|
385
|
+
title: 'Sync Property',
|
|
386
|
+
type: 'object',
|
|
387
|
+
interface: undefined,
|
|
388
|
+
uiSchema: undefined,
|
|
389
|
+
paths: ['syncProp'],
|
|
390
|
+
parentTitles: undefined,
|
|
391
|
+
disabled: false,
|
|
392
|
+
disabledReason: undefined,
|
|
393
|
+
children: [
|
|
394
|
+
{
|
|
395
|
+
name: 'field1',
|
|
396
|
+
title: 'Field 1',
|
|
397
|
+
type: 'string',
|
|
398
|
+
interface: undefined,
|
|
399
|
+
uiSchema: undefined,
|
|
400
|
+
paths: ['syncProp', 'field1'],
|
|
401
|
+
parentTitles: ['Sync Property'],
|
|
402
|
+
disabled: false,
|
|
403
|
+
disabledReason: undefined,
|
|
404
|
+
children: undefined,
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: 'field2',
|
|
408
|
+
title: 'Field 2',
|
|
409
|
+
type: 'number',
|
|
410
|
+
interface: undefined,
|
|
411
|
+
uiSchema: undefined,
|
|
412
|
+
paths: ['syncProp', 'field2'],
|
|
413
|
+
parentTitles: ['Sync Property'],
|
|
414
|
+
disabled: false,
|
|
415
|
+
disabledReason: undefined,
|
|
416
|
+
children: undefined,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// 检查异步属性
|
|
422
|
+
expect(tree[1].name).toBe('asyncProp');
|
|
423
|
+
expect(tree[1].title).toBe('Async Property');
|
|
424
|
+
expect(typeof tree[1].children).toBe('function');
|
|
425
|
+
|
|
426
|
+
// 调用异步函数获取子节点
|
|
427
|
+
const asyncChildren = await (tree[1].children as () => Promise<any>)();
|
|
428
|
+
expect(asyncChildren).toEqual([
|
|
429
|
+
{
|
|
430
|
+
name: 'dynamicField1',
|
|
431
|
+
title: 'Dynamic Field 1',
|
|
432
|
+
type: 'string',
|
|
433
|
+
interface: undefined,
|
|
434
|
+
uiSchema: undefined,
|
|
435
|
+
paths: ['asyncProp', 'dynamicField1'],
|
|
436
|
+
parentTitles: ['Async Property'],
|
|
437
|
+
disabled: false,
|
|
438
|
+
disabledReason: undefined,
|
|
439
|
+
children: undefined,
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'dynamicField2',
|
|
443
|
+
title: 'Dynamic Field 2',
|
|
444
|
+
type: 'boolean',
|
|
445
|
+
interface: undefined,
|
|
446
|
+
uiSchema: undefined,
|
|
447
|
+
paths: ['asyncProp', 'dynamicField2'],
|
|
448
|
+
parentTitles: ['Async Property'],
|
|
449
|
+
disabled: false,
|
|
450
|
+
disabledReason: undefined,
|
|
451
|
+
children: undefined,
|
|
452
|
+
},
|
|
453
|
+
]);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should support multi-level mixed sync and async properties in getPropertyMetaTree', async () => {
|
|
457
|
+
const ctx = new FlowContext();
|
|
458
|
+
|
|
459
|
+
ctx.defineProperty('complexProp', {
|
|
460
|
+
meta: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
title: 'Complex Property',
|
|
463
|
+
properties: {
|
|
464
|
+
// 第一层:同步属性包含异步子属性
|
|
465
|
+
syncWithAsync: {
|
|
466
|
+
type: 'object',
|
|
467
|
+
title: 'Sync with Async',
|
|
468
|
+
properties: async () => {
|
|
469
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
470
|
+
return {
|
|
471
|
+
asyncChild: { type: 'string', title: 'Async Child' },
|
|
472
|
+
syncChild: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
title: 'Sync Child',
|
|
475
|
+
properties: {
|
|
476
|
+
deepField: { type: 'number', title: 'Deep Field' },
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
// 第一层:异步属性包含同步和异步子属性
|
|
483
|
+
asyncWithMixed: {
|
|
484
|
+
type: 'object',
|
|
485
|
+
title: 'Async with Mixed',
|
|
486
|
+
properties: async () => {
|
|
487
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
488
|
+
return {
|
|
489
|
+
syncChild: {
|
|
490
|
+
type: 'string',
|
|
491
|
+
title: 'Sync Child in Async',
|
|
492
|
+
properties: {
|
|
493
|
+
deepSync: { type: 'boolean', title: 'Deep Sync' },
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
asyncChild: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
title: 'Async Child in Async',
|
|
499
|
+
properties: async () => {
|
|
500
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
501
|
+
return {
|
|
502
|
+
veryDeep: { type: 'string', title: 'Very Deep Field' },
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const tree = ctx.getPropertyMetaTree();
|
|
514
|
+
const complexNode = tree[0];
|
|
515
|
+
|
|
516
|
+
expect(complexNode.name).toBe('complexProp');
|
|
517
|
+
expect(complexNode.title).toBe('Complex Property');
|
|
518
|
+
expect(Array.isArray(complexNode.children)).toBe(true);
|
|
519
|
+
|
|
520
|
+
const children = complexNode.children as any[];
|
|
521
|
+
expect(children).toHaveLength(2);
|
|
522
|
+
|
|
523
|
+
// 检查第一个子节点(同步属性包含异步子属性)
|
|
524
|
+
const syncWithAsyncNode = children[0];
|
|
525
|
+
expect(syncWithAsyncNode.name).toBe('syncWithAsync');
|
|
526
|
+
expect(typeof syncWithAsyncNode.children).toBe('function');
|
|
527
|
+
|
|
528
|
+
const syncWithAsyncChildren = await syncWithAsyncNode.children();
|
|
529
|
+
expect(syncWithAsyncChildren).toHaveLength(2);
|
|
530
|
+
expect(syncWithAsyncChildren[0].name).toBe('asyncChild');
|
|
531
|
+
expect(syncWithAsyncChildren[1].name).toBe('syncChild');
|
|
532
|
+
expect(Array.isArray(syncWithAsyncChildren[1].children)).toBe(true);
|
|
533
|
+
|
|
534
|
+
// 检查第二个子节点(异步属性包含混合子属性)
|
|
535
|
+
const asyncWithMixedNode = children[1];
|
|
536
|
+
expect(asyncWithMixedNode.name).toBe('asyncWithMixed');
|
|
537
|
+
expect(typeof asyncWithMixedNode.children).toBe('function');
|
|
538
|
+
|
|
539
|
+
const asyncWithMixedChildren = await asyncWithMixedNode.children();
|
|
540
|
+
expect(asyncWithMixedChildren).toHaveLength(2);
|
|
541
|
+
|
|
542
|
+
// 检查同步子节点
|
|
543
|
+
const syncChildInAsync = asyncWithMixedChildren[0];
|
|
544
|
+
expect(syncChildInAsync.name).toBe('syncChild');
|
|
545
|
+
expect(Array.isArray(syncChildInAsync.children)).toBe(true);
|
|
546
|
+
|
|
547
|
+
// 检查异步子节点
|
|
548
|
+
const asyncChildInAsync = asyncWithMixedChildren[1];
|
|
549
|
+
expect(asyncChildInAsync.name).toBe('asyncChild');
|
|
550
|
+
expect(typeof asyncChildInAsync.children).toBe('function');
|
|
551
|
+
|
|
552
|
+
const veryDeepChildren = await asyncChildInAsync.children();
|
|
553
|
+
expect(veryDeepChildren).toHaveLength(1);
|
|
554
|
+
expect(veryDeepChildren[0].name).toBe('veryDeep');
|
|
555
|
+
expect(veryDeepChildren[0].title).toBe('Very Deep Field');
|
|
556
|
+
});
|
|
557
|
+
it('should define and call instance method with defineMethod', () => {
|
|
558
|
+
const ctx = new FlowContext();
|
|
559
|
+
ctx.defineMethod('hello', function (name: string) {
|
|
560
|
+
return `Hello, ${name}!`;
|
|
561
|
+
});
|
|
562
|
+
expect(ctx.hello('World')).toBe('Hello, World!');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should support method lookup and this binding in delegate chain', () => {
|
|
566
|
+
const delegate = new FlowContext();
|
|
567
|
+
delegate.defineMethod('add', function (a: number, b: number) {
|
|
568
|
+
// this 指向 delegate
|
|
569
|
+
return a + b + (this.extra || 0);
|
|
570
|
+
});
|
|
571
|
+
delegate.extra = 1;
|
|
572
|
+
expect(delegate.add(1, 2)).toBe(4);
|
|
573
|
+
|
|
574
|
+
const ctx = new FlowContext();
|
|
575
|
+
ctx.addDelegate(delegate);
|
|
576
|
+
ctx.extra = 10;
|
|
577
|
+
|
|
578
|
+
expect(ctx.add(1, 2)).toBe(13);
|
|
579
|
+
// 确认 this 绑定到 delegate
|
|
580
|
+
ctx.extra = 100;
|
|
581
|
+
expect(ctx.add(1, 2)).toBe(103);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should allow local defineMethod to override delegate method', () => {
|
|
585
|
+
const delegate = new FlowContext();
|
|
586
|
+
delegate.defineMethod('greet', function (name: string) {
|
|
587
|
+
return `Hello from delegate, ${name}`;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const ctx = new FlowContext();
|
|
591
|
+
ctx.addDelegate(delegate);
|
|
592
|
+
|
|
593
|
+
// 覆盖 delegate 的 greet 方法
|
|
594
|
+
ctx.defineMethod('greet', function (name: string) {
|
|
595
|
+
return `Hello from local, ${name}`;
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
expect(ctx.greet('Copilot')).toBe('Hello from local, Copilot');
|
|
599
|
+
// delegate 仍然保持原方法
|
|
600
|
+
expect(delegate.greet('Copilot')).toBe('Hello from delegate, Copilot');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should define and call method that uses property', () => {
|
|
604
|
+
const ctx = new FlowContext();
|
|
605
|
+
ctx.defineProperty('foo', { value: 10 });
|
|
606
|
+
ctx.defineMethod('getFooPlus', function (n: number) {
|
|
607
|
+
return this.foo + n;
|
|
608
|
+
});
|
|
609
|
+
expect(ctx.getFooPlus(5)).toBe(15);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should call delegate method and access delegate property', () => {
|
|
613
|
+
const delegate = new FlowContext();
|
|
614
|
+
delegate.defineProperty('bar', { value: 20 });
|
|
615
|
+
delegate.defineMethod('getBarDouble', function () {
|
|
616
|
+
return this.bar * 2;
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const ctx = new FlowContext();
|
|
620
|
+
ctx.addDelegate(delegate);
|
|
621
|
+
|
|
622
|
+
expect(ctx.getBarDouble()).toBe(40);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should allow local method to access local and delegate properties', () => {
|
|
626
|
+
const delegate = new FlowContext();
|
|
627
|
+
delegate.defineProperty('bar', { value: 7 });
|
|
628
|
+
|
|
629
|
+
const ctx = new FlowContext();
|
|
630
|
+
ctx.addDelegate(delegate);
|
|
631
|
+
ctx.defineProperty('foo', { value: 3 });
|
|
632
|
+
ctx.defineMethod('sumFooBar', function () {
|
|
633
|
+
return ctx.foo + ctx.bar;
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(ctx.sumFooBar()).toBe(10);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should override delegate method and still access delegate property', () => {
|
|
640
|
+
const delegate = new FlowContext();
|
|
641
|
+
delegate.defineProperty('bar', { value: 100 });
|
|
642
|
+
delegate.defineMethod('getBar', function () {
|
|
643
|
+
return this.bar;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const ctx = new FlowContext();
|
|
647
|
+
ctx.addDelegate(delegate);
|
|
648
|
+
ctx.defineMethod('getBar', function () {
|
|
649
|
+
// 访问 delegate 的属性
|
|
650
|
+
return ctx.bar + 1;
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
expect(ctx.getBar()).toBe(101);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('should support multi-level delegate method and property lookup', () => {
|
|
657
|
+
const root = new FlowContext();
|
|
658
|
+
root.defineProperty('num', { value: 5 });
|
|
659
|
+
root.defineMethod('getNum', function () {
|
|
660
|
+
return this.num;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const mid = new FlowContext();
|
|
664
|
+
mid.addDelegate(root);
|
|
665
|
+
|
|
666
|
+
const ctx = new FlowContext();
|
|
667
|
+
ctx.addDelegate(mid);
|
|
668
|
+
|
|
669
|
+
expect(ctx.getNum()).toBe(5);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should support multi-level delegate method and property lookup', () => {
|
|
673
|
+
const root = new FlowContext();
|
|
674
|
+
root.defineMethod('getSelf', function () {
|
|
675
|
+
return this;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const mid = new FlowContext();
|
|
679
|
+
mid.addDelegate(root);
|
|
680
|
+
|
|
681
|
+
const ctx = new FlowContext();
|
|
682
|
+
ctx.addDelegate(mid);
|
|
683
|
+
|
|
684
|
+
expect(root.getSelf()).toBe(root);
|
|
685
|
+
expect(mid.getSelf()).toBe(mid);
|
|
686
|
+
expect(ctx.getSelf()).toBe(ctx);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should support addDelegate and removeDelegate for multiple delegates', () => {
|
|
690
|
+
const d1 = new FlowContext();
|
|
691
|
+
d1.defineProperty('foo', { value: 'from d1' });
|
|
692
|
+
|
|
693
|
+
const d2 = new FlowContext();
|
|
694
|
+
d2.defineProperty('bar', { value: 'from d2' });
|
|
695
|
+
|
|
696
|
+
const ctx = new FlowContext();
|
|
697
|
+
ctx.addDelegate(d1);
|
|
698
|
+
ctx.addDelegate(d2);
|
|
699
|
+
|
|
700
|
+
// 能查到 d1 和 d2 的属性
|
|
701
|
+
expect(ctx.foo).toBe('from d1');
|
|
702
|
+
expect(ctx.bar).toBe('from d2');
|
|
703
|
+
|
|
704
|
+
// 移除 d1 后,foo 查不到
|
|
705
|
+
ctx.removeDelegate(d1);
|
|
706
|
+
expect(ctx.foo).toBeUndefined();
|
|
707
|
+
expect(ctx.bar).toBe('from d2');
|
|
708
|
+
|
|
709
|
+
// 移除 d2 后,bar 也查不到
|
|
710
|
+
ctx.removeDelegate(d2);
|
|
711
|
+
expect(ctx.bar).toBeUndefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('should respect delegate priority: later addDelegate has higher priority', () => {
|
|
715
|
+
const d1 = new FlowContext();
|
|
716
|
+
d1.defineProperty('foo', { value: 'from d1' });
|
|
717
|
+
|
|
718
|
+
const d2 = new FlowContext();
|
|
719
|
+
d2.defineProperty('foo', { value: 'from d2' });
|
|
720
|
+
|
|
721
|
+
const ctx = new FlowContext();
|
|
722
|
+
ctx.addDelegate(d1);
|
|
723
|
+
ctx.addDelegate(d2);
|
|
724
|
+
|
|
725
|
+
// d2 优先
|
|
726
|
+
expect(ctx.foo).toBe('from d2');
|
|
727
|
+
|
|
728
|
+
// 移除 d2 后,d1 生效
|
|
729
|
+
ctx.removeDelegate(d2);
|
|
730
|
+
expect(ctx.foo).toBe('from d1');
|
|
731
|
+
|
|
732
|
+
// 再移除 d1,查不到
|
|
733
|
+
ctx.removeDelegate(d1);
|
|
734
|
+
expect(ctx.foo).toBeUndefined();
|
|
735
|
+
|
|
736
|
+
// 重新添加 d2,再添加 d1,d1 优先
|
|
737
|
+
ctx.addDelegate(d2);
|
|
738
|
+
ctx.addDelegate(d1);
|
|
739
|
+
expect(ctx.foo).toBe('from d1');
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
describe('FlowEngine context', () => {
|
|
744
|
+
it('should support defineProperty on FlowEngine.context', () => {
|
|
745
|
+
const engine = new FlowEngine();
|
|
746
|
+
engine.context.defineProperty('appName', { value: 'NocoBase' });
|
|
747
|
+
expect(engine.context.appName).toBe('NocoBase');
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('engine.context.runAction should resolve action from engine.getAction', async () => {
|
|
751
|
+
const engine = new FlowEngine();
|
|
752
|
+
|
|
753
|
+
engine.registerActions({
|
|
754
|
+
engineOnly: {
|
|
755
|
+
name: 'engineOnly',
|
|
756
|
+
defaultParams: { a: 1 },
|
|
757
|
+
handler: async (ctx: any, params: any) => {
|
|
758
|
+
return {
|
|
759
|
+
hasModel: !!ctx.model,
|
|
760
|
+
sum: (params.a || 0) + (params.b || 0),
|
|
761
|
+
};
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
const ret = await engine.context.runAction('engineOnly', { b: 2 });
|
|
767
|
+
expect(ret).toEqual({ hasModel: false, sum: 3 });
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('engine.context.getAction/getActions should expose global actions', async () => {
|
|
771
|
+
const engine = new FlowEngine();
|
|
772
|
+
const a1 = { name: 'a1', handler: () => 'ok1' };
|
|
773
|
+
const a2 = { name: 'a2', handler: () => 'ok2' };
|
|
774
|
+
engine.registerActions({ a1, a2 });
|
|
775
|
+
|
|
776
|
+
const def = engine.context.getAction('a1');
|
|
777
|
+
expect(def?.name).toBe('a1');
|
|
778
|
+
const actions = engine.context.getActions();
|
|
779
|
+
expect(actions).toBeInstanceOf(Map);
|
|
780
|
+
expect(Array.from(actions.keys())).toEqual(expect.arrayContaining(['a1', 'a2']));
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
describe('ForkFlowModel context inheritance and isolation', () => {
|
|
785
|
+
let engine: FlowEngine;
|
|
786
|
+
class TestModel extends FlowModel {}
|
|
787
|
+
|
|
788
|
+
beforeEach(() => {
|
|
789
|
+
engine = new FlowEngine();
|
|
790
|
+
engine.registerModels({ TestModel });
|
|
791
|
+
engine.context.defineProperty('appName', { value: 'NocoBase' });
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('should inherit engine.context property in FlowModel.context', () => {
|
|
795
|
+
const model = engine.createModel({ use: 'TestModel' });
|
|
796
|
+
expect(model.context.appName).toBe('NocoBase');
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('model.context.runAction should resolve via model.getAction and override global', async () => {
|
|
800
|
+
// 全局注册一个同名 action
|
|
801
|
+
engine.registerActions({
|
|
802
|
+
sum: {
|
|
803
|
+
name: 'sum',
|
|
804
|
+
defaultParams: { x: 100 },
|
|
805
|
+
handler: (ctx, params) => ({ from: 'engine', x: params.x }),
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// 类级别注册同名 action,应该覆盖全局
|
|
810
|
+
TestModel.registerAction({
|
|
811
|
+
name: 'sum',
|
|
812
|
+
defaultParams: { x: 1 },
|
|
813
|
+
handler: (ctx, params) => ({ from: 'model', x: params.x, hasModel: !!ctx.model }),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const model = engine.createModel({ use: 'TestModel' });
|
|
817
|
+
const ret = await model.context.runAction('sum', {});
|
|
818
|
+
expect(ret).toEqual({ from: 'model', x: 1, hasModel: true });
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('model.context.getAction/getActions should prefer model actions over global', async () => {
|
|
822
|
+
// 全局注册 foo / bar
|
|
823
|
+
const engineFoo = { name: 'foo', handler: () => 'engineFoo' };
|
|
824
|
+
const engineBar = { name: 'bar', handler: () => 'engineBar' };
|
|
825
|
+
engine.registerActions({ foo: engineFoo, bar: engineBar });
|
|
826
|
+
|
|
827
|
+
// 类级别覆盖 foo,并新增 baz
|
|
828
|
+
const modelFoo = { name: 'foo', handler: () => 'modelFoo' };
|
|
829
|
+
const modelBaz = { name: 'baz', handler: () => 'modelBaz' };
|
|
830
|
+
(TestModel as typeof FlowModel).registerActions({ foo: modelFoo, baz: modelBaz });
|
|
831
|
+
|
|
832
|
+
const model = engine.createModel({ use: 'TestModel' });
|
|
833
|
+
|
|
834
|
+
// getAction:foo 应该来自 model 级
|
|
835
|
+
const defFoo = model.context.getAction('foo');
|
|
836
|
+
expect(defFoo).toBe(modelFoo);
|
|
837
|
+
|
|
838
|
+
// getActions:应包含 foo(来自 model)、bar(来自 engine)、baz(来自 model)
|
|
839
|
+
const all = model.context.getActions();
|
|
840
|
+
expect(all.get('foo')).toBe(modelFoo);
|
|
841
|
+
expect(all.get('bar')).toBe(engineBar);
|
|
842
|
+
expect(all.get('baz')).toBe(modelBaz);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('should inherit model.context property in ForkFlowModel.context', () => {
|
|
846
|
+
const model = engine.createModel({ use: 'TestModel' });
|
|
847
|
+
const fork = model.createFork();
|
|
848
|
+
expect(fork.context.appName).toBe('NocoBase');
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should inherit latest value after model.context property changes in fork.context', () => {
|
|
852
|
+
const model = engine.createModel({ use: 'TestModel' });
|
|
853
|
+
model.context.defineProperty('appName', { value: 'NocoBase2' });
|
|
854
|
+
const fork = model.createFork();
|
|
855
|
+
expect(fork.context.appName).toBe('NocoBase2');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('should not affect model.context when fork.context property changes', () => {
|
|
859
|
+
const model = engine.createModel({ use: 'TestModel' });
|
|
860
|
+
model.context.defineProperty('appName', { value: 'NocoBase2' });
|
|
861
|
+
const fork = model.createFork();
|
|
862
|
+
fork.context.defineProperty('appName', { value: 'NocoBase3' });
|
|
863
|
+
expect(fork.context.appName).toBe('NocoBase3');
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('should isolate fork.context property changes from subModel.context when subModel delegates to parent', () => {
|
|
867
|
+
const model = engine.createModel({
|
|
868
|
+
use: 'TestModel',
|
|
869
|
+
subModels: {
|
|
870
|
+
sub: { uid: 'sub1', use: 'TestModel' },
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
model.context.defineProperty('appName', { value: 'NocoBase2' });
|
|
874
|
+
const sub = engine.getModel<TestModel>('sub1');
|
|
875
|
+
expect(sub.context.appName).toBe('NocoBase2');
|
|
876
|
+
sub.context.defineProperty('appName', { value: 'NocoBase3' });
|
|
877
|
+
const fork = sub.createFork();
|
|
878
|
+
expect(fork.context.appName).toBe('NocoBase3');
|
|
879
|
+
fork.context.defineProperty('appName', { value: 'NocoBase4' });
|
|
880
|
+
expect(fork.context.appName).toBe('NocoBase4');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('should isolate context property changes between different forks', () => {
|
|
884
|
+
const model = engine.createModel({
|
|
885
|
+
use: 'TestModel',
|
|
886
|
+
subModels: {
|
|
887
|
+
sub: { uid: 'sub1', use: 'TestModel' },
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
model.context.defineProperty('appName', { value: 'NocoBase2' });
|
|
891
|
+
const sub = engine.getModel<TestModel>('sub1');
|
|
892
|
+
expect(sub.context.appName).toBe('NocoBase2');
|
|
893
|
+
sub.context.defineProperty('appName', { value: 'NocoBase3' });
|
|
894
|
+
const fork = sub.createFork();
|
|
895
|
+
const fork2 = sub.createFork();
|
|
896
|
+
expect(fork.context.appName).toBe('NocoBase3');
|
|
897
|
+
expect(fork2.context.appName).toBe('NocoBase3');
|
|
898
|
+
fork.context.defineProperty('appName', { value: 'NocoBase4' });
|
|
899
|
+
fork2.context.defineProperty('appName', { value: 'NocoBase5' });
|
|
900
|
+
expect(fork.context.appName).toBe('NocoBase4');
|
|
901
|
+
expect(fork2.context.appName).toBe('NocoBase5');
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('should not inherit parent context property when subModel disables delegateToParent', () => {
|
|
905
|
+
const model = engine.createModel({
|
|
906
|
+
use: 'TestModel',
|
|
907
|
+
subModels: {
|
|
908
|
+
sub: { uid: 'sub1', use: 'TestModel', delegateToParent: false },
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
model.context.defineProperty('appName', { value: 'NocoBase2' });
|
|
912
|
+
const sub = engine.getModel<TestModel>('sub1');
|
|
913
|
+
expect(sub.context.appName).toBe('NocoBase');
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('should only define property once when once: true', () => {
|
|
917
|
+
const ctx = new FlowContext();
|
|
918
|
+
ctx.defineProperty('foo', { value: 1, once: true });
|
|
919
|
+
ctx.defineProperty('foo', { value: 2 });
|
|
920
|
+
expect(ctx.foo).toBe(1);
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
describe('runAction delegation from runtime context', () => {
|
|
925
|
+
it('ctx.runAction should delegate to model.context.runAction and use runtime ctx for expression/props', async () => {
|
|
926
|
+
const engine = new FlowEngine();
|
|
927
|
+
class M extends FlowModel {}
|
|
928
|
+
engine.registerModels({ M });
|
|
929
|
+
|
|
930
|
+
// 注册一个动作到模型类
|
|
931
|
+
M.registerAction({
|
|
932
|
+
name: 'echo',
|
|
933
|
+
handler: (ctx, params) => ({ mode: ctx.mode, x: params.x }),
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
const model = engine.createModel({ use: 'M' });
|
|
937
|
+
const ctx = new FlowRuntimeContext(model, 'fk');
|
|
938
|
+
|
|
939
|
+
const res = await ctx.runAction('echo', { x: '{{ ctx.model.uid }}' });
|
|
940
|
+
expect(res.mode).toBe('runtime');
|
|
941
|
+
expect(res.x).toBe(model.uid);
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
describe('FlowContext delayed meta loading', () => {
|
|
946
|
+
// 测试场景:属性定义时 meta 为异步函数,首次访问时延迟加载
|
|
947
|
+
// 输入:属性带有异步 meta 函数
|
|
948
|
+
// 期望:getPropertyMetaTree() 同步返回,节点包含异步 children 函数
|
|
949
|
+
it('should create lazy-loaded meta tree node with async meta function', async () => {
|
|
950
|
+
const ctx = new FlowContext();
|
|
951
|
+
|
|
952
|
+
// 模拟延迟加载的 meta(如 collection 不可用时的情况)
|
|
953
|
+
const delayedMeta = vi.fn(async () => {
|
|
954
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
955
|
+
return {
|
|
956
|
+
type: 'object',
|
|
957
|
+
title: 'Delayed User',
|
|
958
|
+
properties: {
|
|
959
|
+
id: { type: 'number', title: 'User ID' },
|
|
960
|
+
name: { type: 'string', title: 'Username' },
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
ctx.defineProperty('userAsync', {
|
|
966
|
+
meta: delayedMeta,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// 同步获取 meta tree
|
|
970
|
+
const tree = ctx.getPropertyMetaTree();
|
|
971
|
+
|
|
972
|
+
expect(tree).toHaveLength(1);
|
|
973
|
+
const userNode = tree[0];
|
|
974
|
+
|
|
975
|
+
// 验证延迟加载节点的默认属性
|
|
976
|
+
expect(userNode.name).toBe('userAsync');
|
|
977
|
+
expect(userNode.title).toBe('userAsync'); // 默认使用 name
|
|
978
|
+
expect(userNode.type).toBe('object'); // 默认类型
|
|
979
|
+
expect(typeof userNode.children).toBe('function'); // 异步加载函数
|
|
980
|
+
|
|
981
|
+
// 此时还未调用 meta 函数
|
|
982
|
+
expect(delayedMeta).not.toHaveBeenCalled();
|
|
983
|
+
|
|
984
|
+
// 首次访问 children 时才调用 meta 函数
|
|
985
|
+
const children = await (userNode.children as () => Promise<any>)();
|
|
986
|
+
expect(delayedMeta).toHaveBeenCalledTimes(1);
|
|
987
|
+
|
|
988
|
+
// 验证加载后的子节点
|
|
989
|
+
expect(children).toHaveLength(2);
|
|
990
|
+
expect(children[0]).toEqual({
|
|
991
|
+
name: 'id',
|
|
992
|
+
title: 'User ID',
|
|
993
|
+
type: 'number',
|
|
994
|
+
interface: undefined,
|
|
995
|
+
uiSchema: undefined,
|
|
996
|
+
paths: ['userAsync', 'id'],
|
|
997
|
+
parentTitles: ['Delayed User'],
|
|
998
|
+
disabled: false,
|
|
999
|
+
disabledReason: undefined,
|
|
1000
|
+
children: undefined,
|
|
1001
|
+
});
|
|
1002
|
+
expect(children[1]).toEqual({
|
|
1003
|
+
name: 'name',
|
|
1004
|
+
title: 'Username',
|
|
1005
|
+
type: 'string',
|
|
1006
|
+
interface: undefined,
|
|
1007
|
+
uiSchema: undefined,
|
|
1008
|
+
paths: ['userAsync', 'name'],
|
|
1009
|
+
parentTitles: ['Delayed User'],
|
|
1010
|
+
disabled: false,
|
|
1011
|
+
disabledReason: undefined,
|
|
1012
|
+
children: undefined,
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// 测试场景:异步 meta 函数抛出异常时的错误处理
|
|
1017
|
+
// 输入:meta 函数抛出错误(如网络异常)
|
|
1018
|
+
// 期望:children 函数返回空数组,记录警告但不中断程序
|
|
1019
|
+
it('should handle async meta function errors gracefully', async () => {
|
|
1020
|
+
const ctx = new FlowContext();
|
|
1021
|
+
|
|
1022
|
+
const failingMeta = vi.fn(async () => {
|
|
1023
|
+
throw new Error('Collection load failed');
|
|
1024
|
+
});
|
|
1025
|
+
ctx.defineProperty('errorProp', { meta: failingMeta });
|
|
1026
|
+
|
|
1027
|
+
const tree = ctx.getPropertyMetaTree();
|
|
1028
|
+
const node = tree[0];
|
|
1029
|
+
|
|
1030
|
+
// 模拟 console.warn 以验证错误处理
|
|
1031
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1032
|
+
|
|
1033
|
+
const children = await (node.children as () => Promise<any>)();
|
|
1034
|
+
expect(children).toEqual([]);
|
|
1035
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to load meta for errorProp:', expect.any(Error));
|
|
1036
|
+
|
|
1037
|
+
consoleSpy.mockRestore();
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// 测试场景:异步 meta 包含嵌套异步 properties 的深度延迟加载
|
|
1041
|
+
// 输入:异步 meta 返回的 properties 本身也是异步函数
|
|
1042
|
+
// 期望:支持多层异步嵌套加载,每层按需加载
|
|
1043
|
+
it('should support nested async properties in delayed meta loading', async () => {
|
|
1044
|
+
const ctx = new FlowContext();
|
|
1045
|
+
|
|
1046
|
+
ctx.defineProperty('deepAsync', {
|
|
1047
|
+
meta: async () => ({
|
|
1048
|
+
type: 'object',
|
|
1049
|
+
title: 'Deep Async Root',
|
|
1050
|
+
properties: async () => {
|
|
1051
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1052
|
+
return {
|
|
1053
|
+
level1: {
|
|
1054
|
+
type: 'object',
|
|
1055
|
+
title: 'Level 1',
|
|
1056
|
+
properties: {
|
|
1057
|
+
level2Field: { type: 'string', title: 'Level 2 Field' },
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
},
|
|
1062
|
+
}),
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
const tree = ctx.getPropertyMetaTree();
|
|
1066
|
+
const rootNode = tree[0];
|
|
1067
|
+
|
|
1068
|
+
// 第一层异步加载
|
|
1069
|
+
const level1Children = await (rootNode.children as () => Promise<any>)();
|
|
1070
|
+
expect(level1Children).toHaveLength(1);
|
|
1071
|
+
|
|
1072
|
+
const level1Node = level1Children[0];
|
|
1073
|
+
expect(level1Node.name).toBe('level1');
|
|
1074
|
+
expect(level1Node.title).toBe('Level 1');
|
|
1075
|
+
expect(Array.isArray(level1Node.children)).toBe(true);
|
|
1076
|
+
|
|
1077
|
+
// 第二层直接可用(同步 properties)
|
|
1078
|
+
const level2Children = level1Node.children as any[];
|
|
1079
|
+
expect(level2Children).toHaveLength(1);
|
|
1080
|
+
expect(level2Children[0].name).toBe('level2Field');
|
|
1081
|
+
expect(level2Children[0].title).toBe('Level 2 Field');
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
describe('getPropertyMetaTree with deep delegate meta', () => {
|
|
1086
|
+
it('should resolve meta defined on a deeper delegate when using value path', () => {
|
|
1087
|
+
// bottom defines the actual meta for 'collection'
|
|
1088
|
+
const bottom = new FlowContext();
|
|
1089
|
+
bottom.defineProperty('collection', {
|
|
1090
|
+
meta: {
|
|
1091
|
+
type: 'object',
|
|
1092
|
+
title: 'Collection',
|
|
1093
|
+
properties: {
|
|
1094
|
+
name: { type: 'string', title: 'Name' },
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// middle delegates to bottom (no own meta)
|
|
1100
|
+
const middle = new FlowContext();
|
|
1101
|
+
middle.addDelegate(bottom);
|
|
1102
|
+
|
|
1103
|
+
// top delegates to middle (no own meta)
|
|
1104
|
+
const top = new FlowContext();
|
|
1105
|
+
top.addDelegate(middle);
|
|
1106
|
+
|
|
1107
|
+
// Previously, findMetaByPath only checked first-level delegates and failed here.
|
|
1108
|
+
// After fix, it should find 'collection' meta through deep delegates.
|
|
1109
|
+
const subTree = top.getPropertyMetaTree('{{ ctx.collection }}');
|
|
1110
|
+
expect(Array.isArray(subTree)).toBe(true);
|
|
1111
|
+
expect(subTree.length).toBeGreaterThan(0);
|
|
1112
|
+
// Ensure we actually got the field children of collection
|
|
1113
|
+
const fieldNames = subTree.map((n) => n.name);
|
|
1114
|
+
expect(fieldNames).toContain('name');
|
|
1115
|
+
// Also verify each child has full path starting with ['collection', ...]
|
|
1116
|
+
subTree.forEach((n) => expect(n.paths[0]).toBe('collection'));
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
describe('FlowContext resolveOnServer selective server resolution', () => {
|
|
1121
|
+
it('does not call server by default (no resolveOnServer set)', async () => {
|
|
1122
|
+
const engine = new FlowEngine();
|
|
1123
|
+
const api = { request: vi.fn() } as any;
|
|
1124
|
+
engine.context.defineProperty('api', { value: api });
|
|
1125
|
+
|
|
1126
|
+
engine.context.defineProperty('view', {
|
|
1127
|
+
get: () => ({ record: { id: 2 }, type: 'dialog' }),
|
|
1128
|
+
// default resolveOnServer = false
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const tpl = { id: '{{ ctx.view.record.id }}', type: '{{ ctx.view.type }}' } as any;
|
|
1132
|
+
const out = await (engine.context as any).resolveJsonTemplate(tpl);
|
|
1133
|
+
expect(out).toEqual({ id: 2, type: 'dialog' });
|
|
1134
|
+
expect(api.request).not.toHaveBeenCalled();
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('calls server only for subpaths that match resolveOnServer function', async () => {
|
|
1138
|
+
const engine = new FlowEngine();
|
|
1139
|
+
const api = {
|
|
1140
|
+
request: vi.fn(async (config: any) => {
|
|
1141
|
+
const cp = config?.data?.values?.contextParams || {};
|
|
1142
|
+
// Only 'view.record' should be present
|
|
1143
|
+
expect(Object.keys(cp)).toContain('view.record');
|
|
1144
|
+
return { data: { id: 1 } } as any;
|
|
1145
|
+
}),
|
|
1146
|
+
} as any;
|
|
1147
|
+
engine.context.defineProperty('api', { value: api });
|
|
1148
|
+
|
|
1149
|
+
engine.context.defineProperty('view', {
|
|
1150
|
+
get: () => ({ type: 'dialog' }),
|
|
1151
|
+
resolveOnServer: (p: string) => p === 'record' || p.startsWith('record.'),
|
|
1152
|
+
meta: async () => ({
|
|
1153
|
+
type: 'object',
|
|
1154
|
+
title: 'View',
|
|
1155
|
+
buildVariablesParams: () => ({ record: { collection: 'users', filterByTk: 1, dataSourceKey: 'main' } }),
|
|
1156
|
+
}),
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const tpl = { id: '{{ ctx.view.record.id }}', type: '{{ ctx.view.type }}' } as any;
|
|
1160
|
+
const out = await (engine.context as any).resolveJsonTemplate(tpl);
|
|
1161
|
+
expect(out.type).toBe('dialog');
|
|
1162
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
1163
|
+
const [{ url }] = api.request.mock.calls[0];
|
|
1164
|
+
expect(url).toBe('variables:resolve');
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('calls server for whole variable when resolveOnServer is true', async () => {
|
|
1168
|
+
const engine = new FlowEngine();
|
|
1169
|
+
const api = {
|
|
1170
|
+
request: vi.fn(async (config: any) => {
|
|
1171
|
+
const cp = config?.data?.values?.contextParams || {};
|
|
1172
|
+
expect(Object.keys(cp)).toContain('user');
|
|
1173
|
+
return { data: { userId: 1 } } as any;
|
|
1174
|
+
}),
|
|
1175
|
+
} as any;
|
|
1176
|
+
engine.context.defineProperty('api', { value: api });
|
|
1177
|
+
|
|
1178
|
+
engine.context.defineProperty('user', {
|
|
1179
|
+
value: { id: 1, name: 'tester' },
|
|
1180
|
+
resolveOnServer: true,
|
|
1181
|
+
meta: async () => ({
|
|
1182
|
+
type: 'object',
|
|
1183
|
+
title: 'User',
|
|
1184
|
+
buildVariablesParams: () => ({ collection: 'users', filterByTk: 1, dataSourceKey: 'main' }),
|
|
1185
|
+
}),
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const tpl = { uid: '{{ ctx.user.id }}' } as any;
|
|
1189
|
+
await (engine.context as any).resolveJsonTemplate(tpl);
|
|
1190
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
it('reads resolveOnServer from delegated context properties (e.g., engine.context -> model.context)', async () => {
|
|
1194
|
+
const engine = new FlowEngine();
|
|
1195
|
+
const api = {
|
|
1196
|
+
request: vi.fn(async (config: any) => ({ data: { ok: true } })),
|
|
1197
|
+
} as any;
|
|
1198
|
+
engine.context.defineProperty('api', { value: api });
|
|
1199
|
+
|
|
1200
|
+
// Define 'user' on engine.context only
|
|
1201
|
+
engine.context.defineProperty('user', {
|
|
1202
|
+
value: { id: 3 },
|
|
1203
|
+
resolveOnServer: true,
|
|
1204
|
+
meta: async () => ({
|
|
1205
|
+
type: 'object',
|
|
1206
|
+
title: 'User',
|
|
1207
|
+
// purposefully omit builder to test empty contextParams path
|
|
1208
|
+
}),
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// Create a model and call resolveJsonTemplate on model.context (delegates to engine.context)
|
|
1212
|
+
const M = class extends FlowModel {};
|
|
1213
|
+
engine.registerModels({ M });
|
|
1214
|
+
const model = engine.createModel({ use: 'M' });
|
|
1215
|
+
|
|
1216
|
+
const tpl = { uid: '{{ ctx.user.id }}' } as any;
|
|
1217
|
+
await (model.context as any).resolveJsonTemplate(tpl);
|
|
1218
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it('still calls server when resolveOnServer=true even without meta/buildVariablesParams', async () => {
|
|
1222
|
+
const engine = new FlowEngine();
|
|
1223
|
+
const api = { request: vi.fn() } as any;
|
|
1224
|
+
engine.context.defineProperty('api', { value: api });
|
|
1225
|
+
|
|
1226
|
+
engine.context.defineProperty('x', {
|
|
1227
|
+
value: { a: 1 },
|
|
1228
|
+
resolveOnServer: true,
|
|
1229
|
+
// no meta / no buildVariablesParams
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
const tpl = { a: '{{ ctx.x.a }}' } as any;
|
|
1233
|
+
const out = await (engine.context as any).resolveJsonTemplate(tpl);
|
|
1234
|
+
expect(out).toEqual({ a: 1 });
|
|
1235
|
+
// based on resolveOnServer, still calls server with empty contextParams
|
|
1236
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('mixes server and client variables correctly in one pass', async () => {
|
|
1240
|
+
const engine = new FlowEngine();
|
|
1241
|
+
const api = {
|
|
1242
|
+
request: vi.fn(async (config: any) => {
|
|
1243
|
+
const cp = config?.data?.values?.contextParams || {};
|
|
1244
|
+
expect(Object.keys(cp).sort()).toEqual(['user', 'view.record']);
|
|
1245
|
+
return { data: { ok: true } } as any;
|
|
1246
|
+
}),
|
|
1247
|
+
} as any;
|
|
1248
|
+
engine.context.defineProperty('api', { value: api });
|
|
1249
|
+
|
|
1250
|
+
engine.context.defineProperty('user', {
|
|
1251
|
+
value: { id: 7, name: 'u' },
|
|
1252
|
+
resolveOnServer: true,
|
|
1253
|
+
meta: async () => ({
|
|
1254
|
+
type: 'object',
|
|
1255
|
+
title: 'User',
|
|
1256
|
+
buildVariablesParams: () => ({ collection: 'users', filterByTk: 7, dataSourceKey: 'main' }),
|
|
1257
|
+
}),
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
engine.context.defineProperty('view', {
|
|
1261
|
+
get: () => ({ type: 'dialog' }),
|
|
1262
|
+
resolveOnServer: (p: string) => p === 'record' || p.startsWith('record.'),
|
|
1263
|
+
meta: async () => ({
|
|
1264
|
+
type: 'object',
|
|
1265
|
+
title: 'View',
|
|
1266
|
+
buildVariablesParams: () => ({ record: { collection: 'posts', filterByTk: 11, dataSourceKey: 'main' } }),
|
|
1267
|
+
}),
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
engine.context.defineProperty('role', { value: 'admin' });
|
|
1271
|
+
|
|
1272
|
+
const tpl = {
|
|
1273
|
+
a: '{{ ctx.user.id }}',
|
|
1274
|
+
b: '{{ ctx.view.record.id }}',
|
|
1275
|
+
c: '{{ ctx.view.type }}',
|
|
1276
|
+
d: '{{ ctx.role }}',
|
|
1277
|
+
} as any;
|
|
1278
|
+
const out = await (engine.context as any).resolveJsonTemplate(tpl);
|
|
1279
|
+
// client-resolved fields
|
|
1280
|
+
expect(out.c).toBe('dialog');
|
|
1281
|
+
expect(out.d).toBe('admin');
|
|
1282
|
+
// server was called
|
|
1283
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
describe('FlowContext.getPropertyOptions()', () => {
|
|
1288
|
+
it('returns own property options when defined locally', () => {
|
|
1289
|
+
const ctx = new FlowContext();
|
|
1290
|
+
ctx.defineProperty('alpha', { value: 1, cache: true });
|
|
1291
|
+
const opt = ctx.getPropertyOptions('alpha');
|
|
1292
|
+
expect(opt).toBeTruthy();
|
|
1293
|
+
expect(opt.value).toBe(1);
|
|
1294
|
+
expect(opt.cache).toBe(true);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it('returns delegated property options when not defined locally', () => {
|
|
1298
|
+
const base = new FlowContext();
|
|
1299
|
+
base.defineProperty('beta', { value: 2, resolveOnServer: true });
|
|
1300
|
+
const ctx = new FlowContext();
|
|
1301
|
+
ctx.addDelegate(base);
|
|
1302
|
+
const opt = ctx.getPropertyOptions('beta');
|
|
1303
|
+
expect(opt).toBeTruthy();
|
|
1304
|
+
expect(opt.value).toBe(2);
|
|
1305
|
+
expect(opt.resolveOnServer).toBe(true);
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it('prefers own property options over delegated ones', () => {
|
|
1309
|
+
const base = new FlowContext();
|
|
1310
|
+
base.defineProperty('gamma', { value: 10, resolveOnServer: true });
|
|
1311
|
+
const ctx = new FlowContext();
|
|
1312
|
+
ctx.addDelegate(base);
|
|
1313
|
+
ctx.defineProperty('gamma', { value: 99, resolveOnServer: false });
|
|
1314
|
+
const opt = ctx.getPropertyOptions('gamma');
|
|
1315
|
+
expect(opt).toBeTruthy();
|
|
1316
|
+
expect(opt.value).toBe(99);
|
|
1317
|
+
expect(opt.resolveOnServer).toBe(false);
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
describe('FlowContext getPropertyMetaTree with value parameter', () => {
|
|
1322
|
+
it('should return full tree when no value parameter is provided', () => {
|
|
1323
|
+
const ctx = new FlowContext();
|
|
1324
|
+
ctx.defineProperty('user', {
|
|
1325
|
+
meta: {
|
|
1326
|
+
type: 'object',
|
|
1327
|
+
title: 'User',
|
|
1328
|
+
properties: {
|
|
1329
|
+
id: { type: 'number', title: 'User ID' },
|
|
1330
|
+
name: { type: 'string', title: 'Username' },
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
});
|
|
1334
|
+
ctx.defineProperty('data', {
|
|
1335
|
+
meta: { type: 'string', title: 'Data' },
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
const tree = ctx.getPropertyMetaTree();
|
|
1339
|
+
expect(tree).toHaveLength(2);
|
|
1340
|
+
expect(tree[0].name).toBe('user');
|
|
1341
|
+
expect(tree[1].name).toBe('data');
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
it('should return subtree for valid {{ ctx.propertyName }} format', () => {
|
|
1345
|
+
const ctx = new FlowContext();
|
|
1346
|
+
ctx.defineProperty('user', {
|
|
1347
|
+
meta: {
|
|
1348
|
+
type: 'object',
|
|
1349
|
+
title: 'User',
|
|
1350
|
+
properties: {
|
|
1351
|
+
id: { type: 'number', title: 'User ID' },
|
|
1352
|
+
name: { type: 'string', title: 'Username' },
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
ctx.defineProperty('data', {
|
|
1357
|
+
meta: { type: 'string', title: 'Data' },
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
const subTree = ctx.getPropertyMetaTree('{{ ctx.user }}');
|
|
1361
|
+
expect(subTree).toHaveLength(2);
|
|
1362
|
+
expect(subTree[0]).toEqual({
|
|
1363
|
+
name: 'id',
|
|
1364
|
+
title: 'User ID',
|
|
1365
|
+
type: 'number',
|
|
1366
|
+
interface: undefined,
|
|
1367
|
+
uiSchema: undefined,
|
|
1368
|
+
paths: ['user', 'id'],
|
|
1369
|
+
parentTitles: undefined,
|
|
1370
|
+
disabled: false,
|
|
1371
|
+
disabledReason: undefined,
|
|
1372
|
+
children: undefined,
|
|
1373
|
+
});
|
|
1374
|
+
expect(subTree[1]).toEqual({
|
|
1375
|
+
name: 'name',
|
|
1376
|
+
title: 'Username',
|
|
1377
|
+
type: 'string',
|
|
1378
|
+
interface: undefined,
|
|
1379
|
+
uiSchema: undefined,
|
|
1380
|
+
paths: ['user', 'name'],
|
|
1381
|
+
parentTitles: undefined,
|
|
1382
|
+
disabled: false,
|
|
1383
|
+
disabledReason: undefined,
|
|
1384
|
+
children: undefined,
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
it('should handle spaces in {{ ctx.propertyName }} format', () => {
|
|
1389
|
+
const ctx = new FlowContext();
|
|
1390
|
+
ctx.defineProperty('user', {
|
|
1391
|
+
meta: {
|
|
1392
|
+
type: 'object',
|
|
1393
|
+
title: 'User',
|
|
1394
|
+
properties: {
|
|
1395
|
+
id: { type: 'number', title: 'User ID' },
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
const subTree1 = ctx.getPropertyMetaTree('{{ctx.user}}');
|
|
1401
|
+
const subTree2 = ctx.getPropertyMetaTree('{{ ctx.user }}');
|
|
1402
|
+
const subTree3 = ctx.getPropertyMetaTree('{{ ctx.user }}');
|
|
1403
|
+
|
|
1404
|
+
expect(subTree1).toHaveLength(1);
|
|
1405
|
+
expect(subTree2).toHaveLength(1);
|
|
1406
|
+
expect(subTree3).toHaveLength(1);
|
|
1407
|
+
expect(subTree1[0].name).toBe('id');
|
|
1408
|
+
expect(subTree2[0].name).toBe('id');
|
|
1409
|
+
expect(subTree3[0].name).toBe('id');
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
it('should return empty array for property without children', () => {
|
|
1413
|
+
const ctx = new FlowContext();
|
|
1414
|
+
ctx.defineProperty('simple', {
|
|
1415
|
+
meta: { type: 'string', title: 'Simple' },
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
const subTree = ctx.getPropertyMetaTree('{{ ctx.simple }}');
|
|
1419
|
+
expect(subTree).toEqual([]);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it('should warn and return full tree for unsupported formats', () => {
|
|
1423
|
+
const ctx = new FlowContext();
|
|
1424
|
+
ctx.defineProperty('user', {
|
|
1425
|
+
meta: {
|
|
1426
|
+
type: 'object',
|
|
1427
|
+
title: 'User',
|
|
1428
|
+
properties: {
|
|
1429
|
+
id: { type: 'number', title: 'User ID' },
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
ctx.defineProperty('data', {
|
|
1434
|
+
meta: { type: 'string', title: 'Data' },
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1438
|
+
|
|
1439
|
+
// Test various unsupported formats (should trigger warning)
|
|
1440
|
+
const invalidTestCases = ['user', 'ctx.user', '{{user}}', '{{ user }}', 'invalid format'];
|
|
1441
|
+
|
|
1442
|
+
invalidTestCases.forEach((testCase) => {
|
|
1443
|
+
consoleSpy.mockClear();
|
|
1444
|
+
const result = ctx.getPropertyMetaTree(testCase);
|
|
1445
|
+
|
|
1446
|
+
// Should return empty tree for invalid formats
|
|
1447
|
+
expect(result).toHaveLength(0);
|
|
1448
|
+
|
|
1449
|
+
// Should log warning
|
|
1450
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
1451
|
+
`[FlowContext] getPropertyMetaTree - unsupported value format: "${testCase}". Only "{{ ctx.propertyName }}" format is supported. Returning empty meta tree.`,
|
|
1452
|
+
);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// Test valid root context formats (should NOT trigger warning)
|
|
1456
|
+
const validRootCases = ['{{ ctx }}', '{{ctx}}'];
|
|
1457
|
+
|
|
1458
|
+
validRootCases.forEach((testCase) => {
|
|
1459
|
+
consoleSpy.mockClear();
|
|
1460
|
+
const result = ctx.getPropertyMetaTree(testCase);
|
|
1461
|
+
|
|
1462
|
+
// Should return full tree (since {{ ctx }} means all properties)
|
|
1463
|
+
expect(result).toHaveLength(2);
|
|
1464
|
+
expect(result[0].name).toBe('user');
|
|
1465
|
+
expect(result[1].name).toBe('data');
|
|
1466
|
+
|
|
1467
|
+
// Should NOT log warning for valid formats
|
|
1468
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
consoleSpy.mockRestore();
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
it('should return empty array when property is not found', () => {
|
|
1475
|
+
const ctx = new FlowContext();
|
|
1476
|
+
ctx.defineProperty('user', {
|
|
1477
|
+
meta: { type: 'object', title: 'User' },
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
const result = ctx.getPropertyMetaTree('{{ ctx.nonExistent }}');
|
|
1481
|
+
expect(result).toEqual([]);
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
it('should support async meta with value parameter', async () => {
|
|
1485
|
+
const ctx = new FlowContext();
|
|
1486
|
+
ctx.defineProperty('asyncUser', {
|
|
1487
|
+
meta: async () => ({
|
|
1488
|
+
type: 'object',
|
|
1489
|
+
title: 'Async User',
|
|
1490
|
+
properties: {
|
|
1491
|
+
profile: {
|
|
1492
|
+
type: 'object',
|
|
1493
|
+
title: 'Profile',
|
|
1494
|
+
properties: async () => {
|
|
1495
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1496
|
+
return {
|
|
1497
|
+
bio: { type: 'string', title: 'Biography' },
|
|
1498
|
+
};
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
}),
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
const subTree = ctx.getPropertyMetaTree('{{ ctx.asyncUser }}');
|
|
1506
|
+
const ensureArray = async (v: any) => (typeof v === 'function' ? await v() : v);
|
|
1507
|
+
const arr = await ensureArray(subTree);
|
|
1508
|
+
expect(arr).toHaveLength(1);
|
|
1509
|
+
expect(arr[0].name).toBe('profile');
|
|
1510
|
+
expect(typeof arr[0].children).toBe('function');
|
|
1511
|
+
|
|
1512
|
+
const profileChildren = await (arr[0].children as () => Promise<any>)();
|
|
1513
|
+
expect(profileChildren).toHaveLength(1);
|
|
1514
|
+
expect(profileChildren[0].name).toBe('bio');
|
|
1515
|
+
expect(profileChildren[0].title).toBe('Biography');
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
it('should handle async meta errors with value parameter', async () => {
|
|
1519
|
+
const ctx = new FlowContext();
|
|
1520
|
+
const failingMeta = vi.fn(async () => {
|
|
1521
|
+
throw new Error('Async meta failed');
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
ctx.defineProperty('errorProp', { meta: failingMeta });
|
|
1525
|
+
|
|
1526
|
+
const subTree = ctx.getPropertyMetaTree('{{ ctx.errorProp }}');
|
|
1527
|
+
const ensureArray = async (v: any) => (typeof v === 'function' ? await v() : v);
|
|
1528
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1529
|
+
const arr = await ensureArray(subTree);
|
|
1530
|
+
expect(arr).toEqual([]);
|
|
1531
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to load meta for errorProp:', expect.any(Error));
|
|
1532
|
+
|
|
1533
|
+
consoleSpy.mockRestore();
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
it('should support multi-level path like {{ ctx.user.profile }}', () => {
|
|
1537
|
+
const ctx = new FlowContext();
|
|
1538
|
+
ctx.defineProperty('user', {
|
|
1539
|
+
meta: {
|
|
1540
|
+
type: 'object',
|
|
1541
|
+
title: 'User',
|
|
1542
|
+
properties: {
|
|
1543
|
+
id: { type: 'number', title: 'User ID' },
|
|
1544
|
+
profile: {
|
|
1545
|
+
type: 'object',
|
|
1546
|
+
title: 'User Profile',
|
|
1547
|
+
properties: {
|
|
1548
|
+
bio: { type: 'string', title: 'Biography' },
|
|
1549
|
+
avatar: { type: 'string', title: 'Avatar URL' },
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1553
|
+
},
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
// Test getting profile subtree
|
|
1557
|
+
const profileSubTree = ctx.getPropertyMetaTree('{{ ctx.user.profile }}');
|
|
1558
|
+
expect(profileSubTree).toHaveLength(2);
|
|
1559
|
+
expect(profileSubTree[0]).toEqual({
|
|
1560
|
+
name: 'bio',
|
|
1561
|
+
title: 'Biography',
|
|
1562
|
+
type: 'string',
|
|
1563
|
+
interface: undefined,
|
|
1564
|
+
uiSchema: undefined,
|
|
1565
|
+
paths: ['user', 'profile', 'bio'],
|
|
1566
|
+
parentTitles: ['User', 'User Profile'],
|
|
1567
|
+
disabled: false,
|
|
1568
|
+
disabledReason: undefined,
|
|
1569
|
+
children: undefined,
|
|
1570
|
+
});
|
|
1571
|
+
expect(profileSubTree[1]).toEqual({
|
|
1572
|
+
name: 'avatar',
|
|
1573
|
+
title: 'Avatar URL',
|
|
1574
|
+
type: 'string',
|
|
1575
|
+
interface: undefined,
|
|
1576
|
+
uiSchema: undefined,
|
|
1577
|
+
paths: ['user', 'profile', 'avatar'],
|
|
1578
|
+
parentTitles: ['User', 'User Profile'],
|
|
1579
|
+
disabled: false,
|
|
1580
|
+
disabledReason: undefined,
|
|
1581
|
+
children: undefined,
|
|
1582
|
+
});
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('should support deep multi-level path like {{ ctx.data.user.profile.settings }}', () => {
|
|
1586
|
+
const ctx = new FlowContext();
|
|
1587
|
+
ctx.defineProperty('data', {
|
|
1588
|
+
meta: {
|
|
1589
|
+
type: 'object',
|
|
1590
|
+
title: 'Data',
|
|
1591
|
+
properties: {
|
|
1592
|
+
user: {
|
|
1593
|
+
type: 'object',
|
|
1594
|
+
title: 'User Data',
|
|
1595
|
+
properties: {
|
|
1596
|
+
profile: {
|
|
1597
|
+
type: 'object',
|
|
1598
|
+
title: 'Profile',
|
|
1599
|
+
properties: {
|
|
1600
|
+
settings: {
|
|
1601
|
+
type: 'object',
|
|
1602
|
+
title: 'Settings',
|
|
1603
|
+
properties: {
|
|
1604
|
+
theme: { type: 'string', title: 'Theme' },
|
|
1605
|
+
language: { type: 'string', title: 'Language' },
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
const settingsSubTree = ctx.getPropertyMetaTree('{{ ctx.data.user.profile.settings }}');
|
|
1617
|
+
expect(settingsSubTree).toHaveLength(2);
|
|
1618
|
+
expect(settingsSubTree[0].name).toBe('theme');
|
|
1619
|
+
expect(settingsSubTree[0].title).toBe('Theme');
|
|
1620
|
+
expect(settingsSubTree[1].name).toBe('language');
|
|
1621
|
+
expect(settingsSubTree[1].title).toBe('Language');
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it('should return empty array when multi-level path property does not exist', () => {
|
|
1625
|
+
const ctx = new FlowContext();
|
|
1626
|
+
ctx.defineProperty('user', {
|
|
1627
|
+
meta: {
|
|
1628
|
+
type: 'object',
|
|
1629
|
+
title: 'User',
|
|
1630
|
+
properties: {
|
|
1631
|
+
id: { type: 'number', title: 'User ID' },
|
|
1632
|
+
},
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
ctx.defineProperty('data', {
|
|
1636
|
+
meta: { type: 'string', title: 'Data' },
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// Test path that doesn't exist
|
|
1640
|
+
const result = ctx.getPropertyMetaTree('{{ ctx.user.nonExistent }}');
|
|
1641
|
+
expect(result).toEqual([]); // Should return empty array for non-existent path
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
describe('FlowContext meta sort and factory title', () => {
|
|
1646
|
+
it('should sort root nodes by meta.sort (including factory.sort)', async () => {
|
|
1647
|
+
const ctx = new FlowContext();
|
|
1648
|
+
|
|
1649
|
+
// Static metas with sort
|
|
1650
|
+
ctx.defineProperty('b', { meta: { type: 'string', title: 'B', sort: 10 } });
|
|
1651
|
+
ctx.defineProperty('a', { meta: { type: 'string', title: 'A', sort: 5 } });
|
|
1652
|
+
ctx.defineProperty('c', { meta: { type: 'string', title: 'C' } }); // sort defaults to 0
|
|
1653
|
+
|
|
1654
|
+
// Factory meta with title/sort on the function itself
|
|
1655
|
+
const fFactory: any = async () => ({ type: 'object', title: 'F-resolved', properties: {} });
|
|
1656
|
+
fFactory.title = 'F-initial';
|
|
1657
|
+
fFactory.sort = 15;
|
|
1658
|
+
ctx.defineProperty('f', { meta: fFactory });
|
|
1659
|
+
|
|
1660
|
+
const tree = ctx.getPropertyMetaTree();
|
|
1661
|
+
expect(tree.map((n) => n.name)).toEqual(['f', 'b', 'a', 'c']);
|
|
1662
|
+
|
|
1663
|
+
// factory node uses its function.title before resolve
|
|
1664
|
+
const fNode = tree[0];
|
|
1665
|
+
expect(fNode.name).toBe('f');
|
|
1666
|
+
expect(fNode.title).toBe('F-initial');
|
|
1667
|
+
|
|
1668
|
+
// After resolving children, title should update to resolved meta.title
|
|
1669
|
+
if (typeof fNode.children === 'function') {
|
|
1670
|
+
await (fNode.children as () => Promise<any>)();
|
|
1671
|
+
expect(fNode.title).toBe('F-resolved');
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
it('should sort child nodes by meta.sort (sync properties)', () => {
|
|
1676
|
+
const ctx = new FlowContext();
|
|
1677
|
+
|
|
1678
|
+
ctx.defineProperty('parent', {
|
|
1679
|
+
meta: {
|
|
1680
|
+
type: 'object',
|
|
1681
|
+
title: 'Parent',
|
|
1682
|
+
properties: {
|
|
1683
|
+
c: { type: 'string', title: 'C', sort: 1 },
|
|
1684
|
+
a: { type: 'string', title: 'A', sort: 3 },
|
|
1685
|
+
b: { type: 'string', title: 'B', sort: 2 },
|
|
1686
|
+
},
|
|
1687
|
+
},
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
const subTree = ctx.getPropertyMetaTree('{{ ctx.parent }}');
|
|
1691
|
+
expect(Array.isArray(subTree)).toBe(true);
|
|
1692
|
+
const names = (subTree as any[]).map((n) => n.name);
|
|
1693
|
+
expect(names).toEqual(['a', 'b', 'c']); // desc by sort
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
it('should sort child nodes by meta.sort (async properties)', async () => {
|
|
1697
|
+
const ctx = new FlowContext();
|
|
1698
|
+
|
|
1699
|
+
ctx.defineProperty('asyncParent', {
|
|
1700
|
+
meta: {
|
|
1701
|
+
type: 'object',
|
|
1702
|
+
title: 'AsyncParent',
|
|
1703
|
+
properties: async () => ({
|
|
1704
|
+
c: { type: 'string', title: 'C', sort: 1 },
|
|
1705
|
+
a: { type: 'string', title: 'A', sort: 3 },
|
|
1706
|
+
b: { type: 'string', title: 'B', sort: 2 },
|
|
1707
|
+
}),
|
|
1708
|
+
},
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
const subTree = ctx.getPropertyMetaTree('{{ ctx.asyncParent }}');
|
|
1712
|
+
const nodes = typeof subTree === 'function' ? await (subTree as any)() : subTree;
|
|
1713
|
+
const names = (nodes as any[]).map((n) => n.name);
|
|
1714
|
+
expect(names).toEqual(['a', 'b', 'c']);
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
describe('FlowContext getPropertyMetaTree with complex async/sync mixing scenarios', () => {
|
|
1719
|
+
it('should handle async meta factory with async properties', async () => {
|
|
1720
|
+
const ctx = new FlowContext();
|
|
1721
|
+
|
|
1722
|
+
// 异步 meta factory 包含异步 properties
|
|
1723
|
+
ctx.defineProperty('complexUser', {
|
|
1724
|
+
meta: async () => ({
|
|
1725
|
+
type: 'object',
|
|
1726
|
+
title: 'Complex User',
|
|
1727
|
+
properties: async () => ({
|
|
1728
|
+
profile: {
|
|
1729
|
+
type: 'object',
|
|
1730
|
+
title: 'Profile',
|
|
1731
|
+
properties: {
|
|
1732
|
+
name: { type: 'string', title: 'Name' },
|
|
1733
|
+
age: { type: 'number', title: 'Age' },
|
|
1734
|
+
},
|
|
1735
|
+
},
|
|
1736
|
+
settings: {
|
|
1737
|
+
type: 'object',
|
|
1738
|
+
title: 'Settings',
|
|
1739
|
+
properties: {
|
|
1740
|
+
theme: { type: 'string', title: 'Theme' },
|
|
1741
|
+
notifications: { type: 'boolean', title: 'Notifications' },
|
|
1742
|
+
},
|
|
1743
|
+
},
|
|
1744
|
+
}),
|
|
1745
|
+
}),
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// 新行为:根级返回 children 数组(profile、settings)
|
|
1749
|
+
const rootTree = ctx.getPropertyMetaTree('{{ ctx.complexUser }}');
|
|
1750
|
+
const ensureArray = async (v: any) => (typeof v === 'function' ? await v() : v);
|
|
1751
|
+
const arr = await ensureArray(rootTree);
|
|
1752
|
+
expect(arr).toHaveLength(2);
|
|
1753
|
+
expect(arr[0].name).toBe('profile');
|
|
1754
|
+
expect(arr[1].name).toBe('settings');
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
it('should handle async meta factory with mixed sync/async properties in deep path', async () => {
|
|
1758
|
+
const ctx = new FlowContext();
|
|
1759
|
+
|
|
1760
|
+
// 复杂的嵌套场景
|
|
1761
|
+
ctx.defineProperty('enterprise', {
|
|
1762
|
+
meta: async () => ({
|
|
1763
|
+
type: 'object',
|
|
1764
|
+
title: 'Enterprise',
|
|
1765
|
+
properties: {
|
|
1766
|
+
// 同步 properties
|
|
1767
|
+
info: {
|
|
1768
|
+
type: 'object',
|
|
1769
|
+
title: 'Info',
|
|
1770
|
+
properties: async () => ({
|
|
1771
|
+
// 异步 properties 返回同步结构
|
|
1772
|
+
company: {
|
|
1773
|
+
type: 'object',
|
|
1774
|
+
title: 'Company',
|
|
1775
|
+
properties: {
|
|
1776
|
+
name: { type: 'string', title: 'Company Name' },
|
|
1777
|
+
industry: { type: 'string', title: 'Industry' },
|
|
1778
|
+
},
|
|
1779
|
+
},
|
|
1780
|
+
location: {
|
|
1781
|
+
type: 'object',
|
|
1782
|
+
title: 'Location',
|
|
1783
|
+
properties: async () => ({
|
|
1784
|
+
// 深度异步嵌套
|
|
1785
|
+
address: { type: 'string', title: 'Address' },
|
|
1786
|
+
coordinates: {
|
|
1787
|
+
type: 'object',
|
|
1788
|
+
title: 'Coordinates',
|
|
1789
|
+
properties: {
|
|
1790
|
+
lat: { type: 'number', title: 'Latitude' },
|
|
1791
|
+
lng: { type: 'number', title: 'Longitude' },
|
|
1792
|
+
},
|
|
1793
|
+
},
|
|
1794
|
+
}),
|
|
1795
|
+
},
|
|
1796
|
+
}),
|
|
1797
|
+
},
|
|
1798
|
+
},
|
|
1799
|
+
}),
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1802
|
+
// 测试深度路径:{{ ctx.enterprise.info.company.name }}
|
|
1803
|
+
const companyNameTree = ctx.getPropertyMetaTree('{{ ctx.enterprise.info.company.name }}');
|
|
1804
|
+
expect(companyNameTree).toHaveLength(1);
|
|
1805
|
+
expect(companyNameTree[0].name).toBe('name'); // 解析到了最终的 name 属性
|
|
1806
|
+
expect(companyNameTree[0].title).toBe('name'); // 异步解析的初始 title 是 name
|
|
1807
|
+
expect(companyNameTree[0].type).toBe('object'); // 异步节点的初始类型是 object
|
|
1808
|
+
|
|
1809
|
+
// 测试深度路径:{{ ctx.enterprise.info.location.coordinates.lat }}
|
|
1810
|
+
// 这个路径包含异步函数,会返回包装的异步节点
|
|
1811
|
+
const coordinatesTree = ctx.getPropertyMetaTree('{{ ctx.enterprise.info.location.coordinates.lat }}');
|
|
1812
|
+
expect(coordinatesTree).toHaveLength(1);
|
|
1813
|
+
expect(coordinatesTree[0].name).toBe('lat'); // 最终解析到 lat
|
|
1814
|
+
expect(typeof coordinatesTree[0].children).toBe('function'); // 但仍然是异步的
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
it('should handle sync meta with async properties in deep path', () => {
|
|
1818
|
+
const ctx = new FlowContext();
|
|
1819
|
+
|
|
1820
|
+
// 同步 meta 包含异步 properties
|
|
1821
|
+
ctx.defineProperty('project', {
|
|
1822
|
+
meta: {
|
|
1823
|
+
type: 'object',
|
|
1824
|
+
title: 'Project',
|
|
1825
|
+
properties: {
|
|
1826
|
+
details: {
|
|
1827
|
+
type: 'object',
|
|
1828
|
+
title: 'Details',
|
|
1829
|
+
properties: async () => ({
|
|
1830
|
+
// 异步加载详细信息
|
|
1831
|
+
metadata: {
|
|
1832
|
+
type: 'object',
|
|
1833
|
+
title: 'Metadata',
|
|
1834
|
+
properties: {
|
|
1835
|
+
version: { type: 'string', title: 'Version' },
|
|
1836
|
+
author: { type: 'string', title: 'Author' },
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
dependencies: {
|
|
1840
|
+
type: 'object',
|
|
1841
|
+
title: 'Dependencies',
|
|
1842
|
+
properties: async () => ({
|
|
1843
|
+
// 双重异步嵌套
|
|
1844
|
+
production: {
|
|
1845
|
+
type: 'array',
|
|
1846
|
+
title: 'Production Dependencies',
|
|
1847
|
+
},
|
|
1848
|
+
development: {
|
|
1849
|
+
type: 'array',
|
|
1850
|
+
title: 'Development Dependencies',
|
|
1851
|
+
},
|
|
1852
|
+
}),
|
|
1853
|
+
},
|
|
1854
|
+
}),
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1857
|
+
},
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
// 测试路径:{{ ctx.project.details.metadata.version }}
|
|
1861
|
+
// 新逻辑现在能够处理这种复杂场景了!
|
|
1862
|
+
const versionTree = ctx.getPropertyMetaTree('{{ ctx.project.details.metadata.version }}');
|
|
1863
|
+
expect(versionTree).toHaveLength(1);
|
|
1864
|
+
expect(versionTree[0].name).toBe('version'); // 成功解析到最终属性
|
|
1865
|
+
expect(versionTree[0].title).toBe('version'); // 异步解析的初始 title 是 name
|
|
1866
|
+
expect(versionTree[0].type).toBe('object'); // 异步节点的初始类型是 object
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
it('should handle async meta factory with error in deep path resolution', async () => {
|
|
1870
|
+
const ctx = new FlowContext();
|
|
1871
|
+
|
|
1872
|
+
ctx.defineProperty('errorProne', {
|
|
1873
|
+
meta: async () => ({
|
|
1874
|
+
type: 'object',
|
|
1875
|
+
title: 'Error Prone',
|
|
1876
|
+
properties: {
|
|
1877
|
+
working: {
|
|
1878
|
+
type: 'object',
|
|
1879
|
+
title: 'Working Section',
|
|
1880
|
+
properties: {
|
|
1881
|
+
data: { type: 'string', title: 'Data' },
|
|
1882
|
+
},
|
|
1883
|
+
},
|
|
1884
|
+
broken: {
|
|
1885
|
+
type: 'object',
|
|
1886
|
+
title: 'Broken Section',
|
|
1887
|
+
// 这里故意没有 properties,测试路径解析失败的情况
|
|
1888
|
+
},
|
|
1889
|
+
},
|
|
1890
|
+
}),
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// 测试正常路径 - 新逻辑能直接解析到最终属性!
|
|
1894
|
+
const workingTree = ctx.getPropertyMetaTree('{{ ctx.errorProne.working.data }}');
|
|
1895
|
+
expect(workingTree).toHaveLength(1);
|
|
1896
|
+
expect(workingTree[0].name).toBe('data'); // 直接解析到data属性
|
|
1897
|
+
expect(workingTree[0].title).toBe('data'); // 异步解析的初始 title 是 name
|
|
1898
|
+
expect(workingTree[0].type).toBe('object'); // 异步节点的初始类型是 object
|
|
1899
|
+
|
|
1900
|
+
// 新行为:根级直接返回 working、broken 子节点
|
|
1901
|
+
const rootTree = ctx.getPropertyMetaTree('{{ ctx.errorProne }}');
|
|
1902
|
+
const ensureArray = async (v: any) => (typeof v === 'function' ? await v() : v);
|
|
1903
|
+
const arr = await ensureArray(rootTree);
|
|
1904
|
+
const names = arr.map((n: any) => n.name).sort();
|
|
1905
|
+
expect(names).toEqual(['broken', 'working']);
|
|
1906
|
+
|
|
1907
|
+
// 测试不存在的路径 - 新逻辑会尝试构建异步节点,即使路径可能不存在
|
|
1908
|
+
const brokenTree = ctx.getPropertyMetaTree('{{ ctx.errorProne.broken.nonExistent }}');
|
|
1909
|
+
expect(brokenTree).toHaveLength(1);
|
|
1910
|
+
expect(brokenTree[0].name).toBe('nonExistent');
|
|
1911
|
+
expect(brokenTree[0].title).toBe('nonExistent');
|
|
1912
|
+
expect(typeof brokenTree[0].children).toBe('function'); // 构建了异步解析函数
|
|
1913
|
+
|
|
1914
|
+
// 测试异步解析会优雅失败(因为 broken 没有 properties)
|
|
1915
|
+
if (typeof brokenTree[0].children === 'function') {
|
|
1916
|
+
// 异步解析会失败,但被捕获并返回空数组
|
|
1917
|
+
const result = await brokenTree[0].children();
|
|
1918
|
+
expect(result).toEqual([]); // 错误被优雅处理,返回空数组
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
it('should handle extremely deep async nesting', async () => {
|
|
1923
|
+
const ctx = new FlowContext();
|
|
1924
|
+
|
|
1925
|
+
// 创建深度嵌套的异步结构
|
|
1926
|
+
ctx.defineProperty('deepNest', {
|
|
1927
|
+
meta: async () => ({
|
|
1928
|
+
type: 'object',
|
|
1929
|
+
title: 'Deep Nest',
|
|
1930
|
+
properties: async () => ({
|
|
1931
|
+
level1: {
|
|
1932
|
+
type: 'object',
|
|
1933
|
+
title: 'Level 1',
|
|
1934
|
+
properties: async () => ({
|
|
1935
|
+
level2: {
|
|
1936
|
+
type: 'object',
|
|
1937
|
+
title: 'Level 2',
|
|
1938
|
+
properties: async () => ({
|
|
1939
|
+
level3: {
|
|
1940
|
+
type: 'object',
|
|
1941
|
+
title: 'Level 3',
|
|
1942
|
+
properties: {
|
|
1943
|
+
final: { type: 'string', title: 'Final Value' },
|
|
1944
|
+
},
|
|
1945
|
+
},
|
|
1946
|
+
}),
|
|
1947
|
+
},
|
|
1948
|
+
}),
|
|
1949
|
+
},
|
|
1950
|
+
}),
|
|
1951
|
+
}),
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// 测试深度路径 - 新逻辑能够深入解析到最终属性!
|
|
1955
|
+
const deepTree = ctx.getPropertyMetaTree('{{ ctx.deepNest.level1.level2.level3.final }}');
|
|
1956
|
+
expect(deepTree).toHaveLength(1);
|
|
1957
|
+
expect(deepTree[0].name).toBe('final'); // 直接解析到最终属性!
|
|
1958
|
+
expect(deepTree[0].title).toBe('final'); // 异步解析的初始 title 是 name
|
|
1959
|
+
expect(deepTree[0].type).toBe('object'); // 异步节点的初始类型是 object
|
|
1960
|
+
|
|
1961
|
+
// 测试中间层级的路径
|
|
1962
|
+
const level2Tree = ctx.getPropertyMetaTree('{{ ctx.deepNest.level1.level2 }}');
|
|
1963
|
+
expect(level2Tree).toHaveLength(1);
|
|
1964
|
+
expect(level2Tree[0].name).toBe('level2');
|
|
1965
|
+
expect(typeof level2Tree[0].children).toBe('function'); // 仍然是异步的,因为包含异步嵌套
|
|
1966
|
+
|
|
1967
|
+
// 新行为:根级直接返回 level1 子节点
|
|
1968
|
+
const rootDeepTree = ctx.getPropertyMetaTree('{{ ctx.deepNest }}');
|
|
1969
|
+
const ensureArray = async (v: any) => (typeof v === 'function' ? await v() : v);
|
|
1970
|
+
const arr = await ensureArray(rootDeepTree);
|
|
1971
|
+
expect(arr).toHaveLength(1);
|
|
1972
|
+
expect(arr[0].name).toBe('level1');
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
it('aggregates multiple resolves into one batch request', async () => {
|
|
1976
|
+
const engine = new FlowEngine();
|
|
1977
|
+
const api = {
|
|
1978
|
+
request: vi.fn(async (config: any) => {
|
|
1979
|
+
// Should be batched into one request with values.batch
|
|
1980
|
+
const batch = config?.data?.values?.batch;
|
|
1981
|
+
expect(Array.isArray(batch)).toBe(true);
|
|
1982
|
+
expect(batch.length).toBe(2);
|
|
1983
|
+
// Echo back distinct data for each id to verify mapping
|
|
1984
|
+
const results = batch.map((item: any, idx: number) => ({ id: item.id, data: { ok: idx + 1 } }));
|
|
1985
|
+
return { data: { data: { results } } } as any;
|
|
1986
|
+
}),
|
|
1987
|
+
} as any;
|
|
1988
|
+
engine.context.defineProperty('api', { value: api });
|
|
1989
|
+
|
|
1990
|
+
// Define a server-resolved variable (no builder needed)
|
|
1991
|
+
engine.context.defineProperty('foo', {
|
|
1992
|
+
value: { a: 1 },
|
|
1993
|
+
resolveOnServer: true,
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
// 使用不同的模板键,避免运行期去重(dedup)导致只保留一个 payload
|
|
1997
|
+
const t1 = { a: '{{ ctx.foo.a }}' } as any;
|
|
1998
|
+
const t2 = { b: '{{ ctx.foo.a }}' } as any;
|
|
1999
|
+
|
|
2000
|
+
// Fire two resolves within the same micro-batch window (concurrently)
|
|
2001
|
+
const [r1, r2] = await Promise.all([
|
|
2002
|
+
(engine.context as any).resolveJsonTemplate(t1),
|
|
2003
|
+
(engine.context as any).resolveJsonTemplate(t2),
|
|
2004
|
+
]);
|
|
2005
|
+
|
|
2006
|
+
// API called once with batched payload
|
|
2007
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
2008
|
+
// Ensure both results were mapped back correctly by order (ok:1, ok:2)
|
|
2009
|
+
expect(r1.ok).toBe(1);
|
|
2010
|
+
expect(r2.ok).toBe(2);
|
|
2011
|
+
});
|
|
2012
|
+
});
|