@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,2746 @@
|
|
|
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 { autorun, observable, reaction, reactive } from '@nocobase/flow-engine';
|
|
11
|
+
import { APIClient } from '@nocobase/sdk';
|
|
12
|
+
import { render, waitFor } from '@testing-library/react';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { vi } from 'vitest';
|
|
15
|
+
import { FlowModelRenderer } from '../../components/FlowModelRenderer';
|
|
16
|
+
import { FlowEngine } from '../../flowEngine';
|
|
17
|
+
import type { DefaultStructure, FlowDefinitionOptions, FlowModelOptions, ModelConstructor } from '../../types';
|
|
18
|
+
import { FlowExitException } from '../../utils';
|
|
19
|
+
import { FlowExitAllException } from '../../utils/exceptions';
|
|
20
|
+
import { defineFlow, FlowModel, ModelRenderMode } from '../flowModel';
|
|
21
|
+
import { ForkFlowModel } from '../forkFlowModel';
|
|
22
|
+
|
|
23
|
+
// 全局处理测试中的未处理 Promise rejection
|
|
24
|
+
const originalUnhandledRejection = process.listeners('unhandledRejection');
|
|
25
|
+
process.removeAllListeners('unhandledRejection');
|
|
26
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
27
|
+
// 如果是我们测试中故意抛出的错误,就忽略它
|
|
28
|
+
if (reason instanceof Error && reason.message === 'Test error') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// 其他错误仍然需要处理
|
|
32
|
+
originalUnhandledRejection.forEach((listener) => {
|
|
33
|
+
if (typeof listener === 'function') {
|
|
34
|
+
listener(reason, promise);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// // Mock dependencies
|
|
40
|
+
// vi.mock('uid/secure', () => ({
|
|
41
|
+
// uid: vi.fn(() => 'mock-uid-' + Math.random().toString(36).substring(2, 11)),
|
|
42
|
+
// }));
|
|
43
|
+
|
|
44
|
+
// vi.mock('../forkFlowModel', () => ({
|
|
45
|
+
// ForkFlowModel: vi.fn().mockImplementation(function (master: any, localProps: any, forkId: number) {
|
|
46
|
+
// const instance = {
|
|
47
|
+
// master,
|
|
48
|
+
// localProps,
|
|
49
|
+
// forkId,
|
|
50
|
+
// setProps: vi.fn(),
|
|
51
|
+
// dispose: vi.fn(),
|
|
52
|
+
// disposed: false,
|
|
53
|
+
// };
|
|
54
|
+
// Object.setPrototypeOf(instance, ForkFlowModel.prototype);
|
|
55
|
+
// return instance;
|
|
56
|
+
// }),
|
|
57
|
+
// }));
|
|
58
|
+
|
|
59
|
+
// vi.mock('../../components/settings/wrappers/contextual/StepSettingsDialog', () => ({
|
|
60
|
+
// openStepSettingsDialog: vi.fn(),
|
|
61
|
+
// }));
|
|
62
|
+
|
|
63
|
+
// vi.mock('../../components/settings/wrappers/contextual/StepRequiredSettingsDialog', () => ({
|
|
64
|
+
// openRequiredParamsStepFormDialog: vi.fn(),
|
|
65
|
+
// }));
|
|
66
|
+
|
|
67
|
+
// vi.mock('lodash', async () => {
|
|
68
|
+
// const actual = await vi.importActual('lodash');
|
|
69
|
+
// return {
|
|
70
|
+
// ...actual,
|
|
71
|
+
// debounce: vi.fn((fn) => fn),
|
|
72
|
+
// };
|
|
73
|
+
// });
|
|
74
|
+
|
|
75
|
+
// Helper functions
|
|
76
|
+
const createMockFlowEngine = (): FlowEngine => {
|
|
77
|
+
return new FlowEngine();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const createBasicFlowDefinition = (overrides: Partial<FlowDefinitionOptions> = {}): FlowDefinitionOptions => ({
|
|
81
|
+
key: 'testFlow',
|
|
82
|
+
steps: {
|
|
83
|
+
step1: {
|
|
84
|
+
handler: vi.fn().mockResolvedValue('step1-result'),
|
|
85
|
+
},
|
|
86
|
+
step2: {
|
|
87
|
+
handler: vi.fn().mockResolvedValue('step2-result'),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
...overrides,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const createAutoFlowDefinition = (overrides: Partial<FlowDefinitionOptions> = {}): FlowDefinitionOptions => ({
|
|
94
|
+
key: 'autoFlow',
|
|
95
|
+
sort: 1,
|
|
96
|
+
steps: {
|
|
97
|
+
autoStep: {
|
|
98
|
+
handler: vi.fn().mockResolvedValue('auto-result'),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
...overrides,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const createEventFlowDefinition = (
|
|
105
|
+
eventName: string,
|
|
106
|
+
overrides: Partial<FlowDefinitionOptions> = {},
|
|
107
|
+
): FlowDefinitionOptions => ({
|
|
108
|
+
key: `${eventName}Flow`,
|
|
109
|
+
on: { eventName },
|
|
110
|
+
steps: {
|
|
111
|
+
eventStep: {
|
|
112
|
+
handler: vi.fn().mockResolvedValue('event-result'),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
...overrides,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const createErrorFlowDefinition = (
|
|
119
|
+
errorMessage = 'Test error',
|
|
120
|
+
overrides: Partial<FlowDefinitionOptions> = {},
|
|
121
|
+
): FlowDefinitionOptions => ({
|
|
122
|
+
key: 'errorFlow',
|
|
123
|
+
steps: {
|
|
124
|
+
errorStep: {
|
|
125
|
+
handler: vi.fn().mockRejectedValue(new Error(errorMessage)),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
...overrides,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Test setup
|
|
132
|
+
let flowEngine: FlowEngine;
|
|
133
|
+
let modelOptions: FlowModelOptions;
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
flowEngine = createMockFlowEngine();
|
|
137
|
+
modelOptions = {
|
|
138
|
+
uid: 'test-model-uid',
|
|
139
|
+
flowEngine,
|
|
140
|
+
stepParams: { testFlow: { step1: { param1: 'value1' } } },
|
|
141
|
+
sortIndex: 0,
|
|
142
|
+
subModels: {},
|
|
143
|
+
};
|
|
144
|
+
vi.clearAllMocks();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('FlowModel', () => {
|
|
148
|
+
// ==================== CONSTRUCTOR & INITIALIZATION ====================
|
|
149
|
+
describe('Constructor & Initialization', () => {
|
|
150
|
+
test('should create instance with basic options', () => {
|
|
151
|
+
const model = new FlowModel(modelOptions);
|
|
152
|
+
|
|
153
|
+
expect(model.uid).toBe(modelOptions.uid);
|
|
154
|
+
expect(model.stepParams).toEqual(expect.objectContaining(modelOptions.stepParams));
|
|
155
|
+
expect(model.flowEngine).toBe(modelOptions.flowEngine);
|
|
156
|
+
expect(model.sortIndex).toBe(modelOptions.sortIndex);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should generate uid if not provided', () => {
|
|
160
|
+
const options = { ...modelOptions, uid: undefined };
|
|
161
|
+
const model = new FlowModel(options);
|
|
162
|
+
|
|
163
|
+
expect(model.uid).toBeDefined();
|
|
164
|
+
expect(typeof model.uid).toBe('string');
|
|
165
|
+
expect(model.uid.length).toBeGreaterThan(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should return existing instance if already exists in FlowEngine', () => {
|
|
169
|
+
const firstInstance = new FlowModel(modelOptions);
|
|
170
|
+
flowEngine.getModel = vi.fn().mockReturnValue(firstInstance);
|
|
171
|
+
|
|
172
|
+
const secondInstance = new FlowModel(modelOptions);
|
|
173
|
+
|
|
174
|
+
expect(secondInstance).toBe(firstInstance);
|
|
175
|
+
expect(flowEngine.getModel).toHaveBeenCalledWith(modelOptions.uid);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('should initialize with default values when options are minimal', () => {
|
|
179
|
+
const model = new FlowModel({ flowEngine } as FlowModelOptions);
|
|
180
|
+
|
|
181
|
+
expect(model.props).toBeDefined();
|
|
182
|
+
expect(model.stepParams).toBeDefined();
|
|
183
|
+
expect(model.subModels).toBeDefined();
|
|
184
|
+
expect(model.forks).toBeInstanceOf(Set);
|
|
185
|
+
expect(model.forks.size).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should throw error when flowEngine is missing', () => {
|
|
189
|
+
expect(() => {
|
|
190
|
+
new FlowModel({} as any);
|
|
191
|
+
}).toThrow('FlowModel must be initialized with a FlowEngine instance.');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('should initialize emitter', () => {
|
|
195
|
+
const model = new FlowModel(modelOptions);
|
|
196
|
+
|
|
197
|
+
expect(model.emitter).toBeDefined();
|
|
198
|
+
expect(typeof model.emitter.on).toBe('function');
|
|
199
|
+
expect(typeof model.emitter.emit).toBe('function');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ==================== PROPERTIES MANAGEMENT ====================
|
|
204
|
+
describe('Properties Management', () => {
|
|
205
|
+
let model: FlowModel;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
model = new FlowModel(modelOptions);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('setProps', () => {
|
|
212
|
+
test('should merge props correctly', () => {
|
|
213
|
+
const initialProps = { a: 1, b: 2 };
|
|
214
|
+
model.setProps(initialProps);
|
|
215
|
+
|
|
216
|
+
expect(model.props).toEqual(expect.objectContaining(initialProps));
|
|
217
|
+
|
|
218
|
+
const additionalProps = { b: 3, c: 4 };
|
|
219
|
+
model.setProps(additionalProps);
|
|
220
|
+
|
|
221
|
+
expect(model.props).toEqual(expect.objectContaining({ a: 1, b: 3, c: 4 }));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('should handle null and undefined props', () => {
|
|
225
|
+
const originalProps = { ...model.props };
|
|
226
|
+
|
|
227
|
+
model.setProps(null as any);
|
|
228
|
+
expect(model.props).toEqual(originalProps);
|
|
229
|
+
|
|
230
|
+
model.setProps({ test: 'value' });
|
|
231
|
+
model.setProps(undefined as any);
|
|
232
|
+
expect(model.props).toEqual(expect.objectContaining({ test: 'value' }));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('should handle nested objects', () => {
|
|
236
|
+
const nestedProps = {
|
|
237
|
+
user: { name: 'John', age: 30 },
|
|
238
|
+
settings: { theme: 'dark', lang: 'en' },
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
model.setProps(nestedProps);
|
|
242
|
+
expect(model.props).toEqual(expect.objectContaining(nestedProps));
|
|
243
|
+
|
|
244
|
+
model.setProps({ user: { name: 'Jane', email: 'jane@example.com' } });
|
|
245
|
+
expect(model.props.user).toEqual({ name: 'Jane', email: 'jane@example.com' });
|
|
246
|
+
expect(model.props.settings).toEqual({ theme: 'dark', lang: 'en' });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test.skip('should be reactive', async () => {
|
|
250
|
+
reaction(
|
|
251
|
+
() => model.props.foo, // 观察的字段
|
|
252
|
+
(newProps, oldProps) => {
|
|
253
|
+
console.log('Props changed from', oldProps, 'to', newProps);
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
model.props.foo = 'bar';
|
|
257
|
+
model.props.foo = 'baz';
|
|
258
|
+
model.setProps({ foo: 'bar' });
|
|
259
|
+
model.setProps({ foo: 'baz' });
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('setStepParams', () => {
|
|
264
|
+
test('should merge step parameters correctly', () => {
|
|
265
|
+
const initialParams = {
|
|
266
|
+
flow1: { step1: { param1: 'value1' } },
|
|
267
|
+
flow2: { step2: { param2: 'value2' } },
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
model.setStepParams(initialParams);
|
|
271
|
+
expect(model.stepParams).toEqual(expect.objectContaining(initialParams));
|
|
272
|
+
|
|
273
|
+
const additionalParams = {
|
|
274
|
+
flow1: { step1: { param1: 'updated', param3: 'value3' } },
|
|
275
|
+
flow3: { step3: { param4: 'value4' } },
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
model.setStepParams(additionalParams);
|
|
279
|
+
|
|
280
|
+
expect(model.stepParams).toEqual(
|
|
281
|
+
expect.objectContaining({
|
|
282
|
+
flow1: { step1: { param1: 'updated', param3: 'value3' } },
|
|
283
|
+
flow2: { step2: { param2: 'value2' } },
|
|
284
|
+
flow3: { step3: { param4: 'value4' } },
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('should handle empty and null parameters', () => {
|
|
290
|
+
const originalParams = { ...model.stepParams };
|
|
291
|
+
|
|
292
|
+
model.setStepParams({});
|
|
293
|
+
expect(model.stepParams).toEqual(originalParams);
|
|
294
|
+
|
|
295
|
+
model.setStepParams(null as any);
|
|
296
|
+
expect(model.stepParams).toEqual(originalParams);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ==================== FLOW MANAGEMENT ====================
|
|
302
|
+
describe('Flow Management', () => {
|
|
303
|
+
// TODO: design and add tests for flows management
|
|
304
|
+
let TestFlowModel: typeof FlowModel;
|
|
305
|
+
|
|
306
|
+
beforeEach(() => {
|
|
307
|
+
TestFlowModel = class extends FlowModel<any> {};
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('placeholder test - should create FlowModel subclass', () => {
|
|
311
|
+
expect(TestFlowModel).toBeDefined();
|
|
312
|
+
expect(TestFlowModel.prototype).toBeInstanceOf(FlowModel);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ==================== FLOW EXECUTION ====================
|
|
317
|
+
describe('Flow Execution', () => {
|
|
318
|
+
let model: FlowModel;
|
|
319
|
+
let TestFlowModel: typeof FlowModel<DefaultStructure>;
|
|
320
|
+
|
|
321
|
+
beforeEach(() => {
|
|
322
|
+
TestFlowModel = class extends FlowModel {};
|
|
323
|
+
model = new TestFlowModel(modelOptions);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('applyFlow', () => {
|
|
327
|
+
test('should throw error for non-existent flow', async () => {
|
|
328
|
+
await expect(model.applyFlow('nonExistentFlow')).rejects.toThrow("Flow 'nonExistentFlow' not found.");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('should throw error when FlowEngine not available', async () => {
|
|
332
|
+
// Since FlowModel constructor now requires flowEngine, we test the error at construction time
|
|
333
|
+
expect(() => {
|
|
334
|
+
new TestFlowModel({ uid: 'test' } as any);
|
|
335
|
+
}).toThrow('FlowModel must be initialized with a FlowEngine instance.');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('should handle FlowExitException correctly', async () => {
|
|
339
|
+
const exitFlow: FlowDefinitionOptions = {
|
|
340
|
+
key: 'exitFlow',
|
|
341
|
+
steps: {
|
|
342
|
+
step1: {
|
|
343
|
+
handler: (ctx) => {
|
|
344
|
+
ctx.exit();
|
|
345
|
+
return 'should-not-reach';
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
step2: {
|
|
349
|
+
handler: vi.fn().mockReturnValue('step2-result'),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
TestFlowModel.registerFlow(exitFlow);
|
|
355
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
356
|
+
|
|
357
|
+
const result = await model.applyFlow('exitFlow');
|
|
358
|
+
|
|
359
|
+
expect(result).toEqual({});
|
|
360
|
+
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
361
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
362
|
+
|
|
363
|
+
consoleSpy.mockRestore();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('should handle FlowExitException correctly', async () => {
|
|
367
|
+
const exitFlow: FlowDefinitionOptions = {
|
|
368
|
+
key: 'exitFlow',
|
|
369
|
+
steps: {
|
|
370
|
+
step1: {
|
|
371
|
+
handler: (ctx) => {
|
|
372
|
+
ctx.exit();
|
|
373
|
+
return 'should-not-reach';
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
step2: {
|
|
377
|
+
handler: vi.fn().mockReturnValue('step2-result'),
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const exitFlow2: FlowDefinitionOptions = {
|
|
383
|
+
key: 'exitFlow2',
|
|
384
|
+
steps: {
|
|
385
|
+
step2: {
|
|
386
|
+
handler: vi.fn().mockReturnValue('step2-result'),
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
TestFlowModel.registerFlow(exitFlow);
|
|
392
|
+
TestFlowModel.registerFlow(exitFlow2);
|
|
393
|
+
const loggerSpy = vi.spyOn(model.context.logger, 'info').mockImplementation(() => {});
|
|
394
|
+
|
|
395
|
+
await model.applyAutoFlows();
|
|
396
|
+
|
|
397
|
+
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
398
|
+
expect(exitFlow2.steps.step2.handler).toHaveBeenCalled();
|
|
399
|
+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowEngine]'));
|
|
400
|
+
|
|
401
|
+
loggerSpy.mockRestore();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('should handle FlowExitAllException correctly', async () => {
|
|
405
|
+
const exitFlow: FlowDefinitionOptions = {
|
|
406
|
+
key: 'exitFlow',
|
|
407
|
+
steps: {
|
|
408
|
+
step1: {
|
|
409
|
+
handler: (ctx) => {
|
|
410
|
+
ctx.exitAll();
|
|
411
|
+
return 'should-not-reach';
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
step2: {
|
|
415
|
+
handler: vi.fn().mockReturnValue('step2-result'),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const exitFlow2: FlowDefinitionOptions = {
|
|
421
|
+
key: 'exitFlow2',
|
|
422
|
+
steps: {
|
|
423
|
+
step2: {
|
|
424
|
+
handler: vi.fn().mockReturnValue('step2-result'),
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
TestFlowModel.registerFlow(exitFlow);
|
|
430
|
+
TestFlowModel.registerFlow(exitFlow2);
|
|
431
|
+
const loggerSpy = vi.spyOn(model.context.logger, 'info').mockImplementation(() => {});
|
|
432
|
+
|
|
433
|
+
await model.applyAutoFlows();
|
|
434
|
+
|
|
435
|
+
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
436
|
+
expect(exitFlow2.steps.step2.handler).not.toHaveBeenCalled();
|
|
437
|
+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowEngine]'));
|
|
438
|
+
|
|
439
|
+
loggerSpy.mockRestore();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('should handle FlowExitAllException correctly', async () => {
|
|
443
|
+
const exitFlow: FlowDefinitionOptions = {
|
|
444
|
+
key: 'exitFlow',
|
|
445
|
+
steps: {
|
|
446
|
+
step1: {
|
|
447
|
+
handler: (ctx) => {
|
|
448
|
+
ctx.exitAll();
|
|
449
|
+
return 'should-not-reach';
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
step2: {
|
|
453
|
+
handler: vi.fn().mockReturnValue('step2-result'),
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
TestFlowModel.registerFlow(exitFlow);
|
|
459
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
460
|
+
|
|
461
|
+
const result = await model.applyFlow('exitFlow');
|
|
462
|
+
|
|
463
|
+
expect(result).toBeInstanceOf(FlowExitAllException);
|
|
464
|
+
expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
|
|
465
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
|
|
466
|
+
|
|
467
|
+
consoleSpy.mockRestore();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('should propagate step execution errors', async () => {
|
|
471
|
+
const errorFlow = createErrorFlowDefinition('Step execution failed');
|
|
472
|
+
TestFlowModel.registerFlow(errorFlow);
|
|
473
|
+
|
|
474
|
+
await expect(model.applyFlow(errorFlow.key)).rejects.toThrow('Step execution failed');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('should use action when step references registered action', async () => {
|
|
478
|
+
const actionHandler = vi.fn().mockResolvedValue('action-result');
|
|
479
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
480
|
+
handler: actionHandler,
|
|
481
|
+
defaultParams: { actionParam: 'actionValue' },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const actionFlow: FlowDefinitionOptions = {
|
|
485
|
+
key: 'actionFlow',
|
|
486
|
+
steps: {
|
|
487
|
+
actionStep: {
|
|
488
|
+
use: 'testAction',
|
|
489
|
+
defaultParams: { stepParam: 'stepValue' },
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
TestFlowModel.registerFlow(actionFlow);
|
|
495
|
+
|
|
496
|
+
const result = await model.applyFlow('actionFlow');
|
|
497
|
+
|
|
498
|
+
expect(model.flowEngine.getAction).toHaveBeenCalledWith('testAction');
|
|
499
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
500
|
+
expect.any(Object),
|
|
501
|
+
expect.objectContaining({
|
|
502
|
+
actionParam: 'actionValue',
|
|
503
|
+
stepParam: 'stepValue',
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
expect(result.actionStep).toBe('action-result');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('should skip step when action not found', async () => {
|
|
510
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue(null);
|
|
511
|
+
const loggerSpy = vi.spyOn(model.context.logger, 'error').mockImplementation(() => {});
|
|
512
|
+
|
|
513
|
+
const actionFlow: FlowDefinitionOptions = {
|
|
514
|
+
key: 'actionFlow',
|
|
515
|
+
steps: {
|
|
516
|
+
missingActionStep: {
|
|
517
|
+
use: 'nonExistentAction',
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
TestFlowModel.registerFlow(actionFlow);
|
|
523
|
+
|
|
524
|
+
const result = await model.applyFlow('actionFlow');
|
|
525
|
+
|
|
526
|
+
expect(result).toEqual({});
|
|
527
|
+
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining("Action 'nonExistentAction' not found"));
|
|
528
|
+
|
|
529
|
+
loggerSpy.mockRestore();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('applyAutoFlows', () => {
|
|
534
|
+
test('should execute all auto flows', async () => {
|
|
535
|
+
const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1', sort: 1 };
|
|
536
|
+
const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2', sort: 2 };
|
|
537
|
+
const manualFlow = { ...createBasicFlowDefinition(), manual: true }; // Mark as manual flow
|
|
538
|
+
|
|
539
|
+
TestFlowModel.registerFlow(autoFlow1);
|
|
540
|
+
TestFlowModel.registerFlow(autoFlow2);
|
|
541
|
+
TestFlowModel.registerFlow(manualFlow);
|
|
542
|
+
|
|
543
|
+
const results = await model.applyAutoFlows();
|
|
544
|
+
|
|
545
|
+
expect(results).toHaveLength(2);
|
|
546
|
+
expect(autoFlow1.steps.autoStep.handler).toHaveBeenCalled();
|
|
547
|
+
expect(autoFlow2.steps.autoStep.handler).toHaveBeenCalled();
|
|
548
|
+
expect(manualFlow.steps.step1.handler).not.toHaveBeenCalled();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test('should execute auto flows in sort order', async () => {
|
|
552
|
+
const executionOrder: string[] = [];
|
|
553
|
+
|
|
554
|
+
const autoFlow1 = {
|
|
555
|
+
key: 'auto1',
|
|
556
|
+
sort: 3,
|
|
557
|
+
steps: {
|
|
558
|
+
step: {
|
|
559
|
+
handler: () => {
|
|
560
|
+
executionOrder.push('auto1');
|
|
561
|
+
return 'result1';
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const autoFlow2 = {
|
|
568
|
+
key: 'auto2',
|
|
569
|
+
sort: 1,
|
|
570
|
+
steps: {
|
|
571
|
+
step: {
|
|
572
|
+
handler: () => {
|
|
573
|
+
executionOrder.push('auto2');
|
|
574
|
+
return 'result2';
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const autoFlow3 = {
|
|
581
|
+
key: 'auto3',
|
|
582
|
+
sort: 2,
|
|
583
|
+
steps: {
|
|
584
|
+
step: {
|
|
585
|
+
handler: () => {
|
|
586
|
+
executionOrder.push('auto3');
|
|
587
|
+
return 'result3';
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
TestFlowModel.registerFlow(autoFlow1);
|
|
594
|
+
TestFlowModel.registerFlow(autoFlow2);
|
|
595
|
+
TestFlowModel.registerFlow(autoFlow3);
|
|
596
|
+
|
|
597
|
+
await model.applyAutoFlows();
|
|
598
|
+
|
|
599
|
+
expect(executionOrder).toEqual(['auto2', 'auto3', 'auto1']);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('should no results when no auto flows found', async () => {
|
|
603
|
+
const results = await model.applyAutoFlows();
|
|
604
|
+
|
|
605
|
+
expect(results).toEqual([]);
|
|
606
|
+
// Note: Log output may be captured in stderr, not console.log
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
describe('lifecycle hooks', () => {
|
|
610
|
+
let TestFlowModelWithHooks: any;
|
|
611
|
+
let beforeHookSpy: any;
|
|
612
|
+
let afterHookSpy: any;
|
|
613
|
+
let errorHookSpy: any;
|
|
614
|
+
|
|
615
|
+
beforeEach(() => {
|
|
616
|
+
beforeHookSpy = vi.fn();
|
|
617
|
+
afterHookSpy = vi.fn();
|
|
618
|
+
errorHookSpy = vi.fn();
|
|
619
|
+
TestFlowModelWithHooks = class extends TestFlowModel {
|
|
620
|
+
async onBeforeAutoFlows(inputArgs?: Record<string, any>) {
|
|
621
|
+
beforeHookSpy(inputArgs);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>) {
|
|
625
|
+
afterHookSpy(results, inputArgs);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>) {
|
|
629
|
+
errorHookSpy(error, inputArgs);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('should call lifecycle hooks in correct order', async () => {
|
|
635
|
+
const autoFlow = createAutoFlowDefinition();
|
|
636
|
+
TestFlowModelWithHooks.registerFlow(autoFlow);
|
|
637
|
+
|
|
638
|
+
const modelWithHooks = new TestFlowModelWithHooks(modelOptions);
|
|
639
|
+
const inputArgs = { test: 'value' };
|
|
640
|
+
|
|
641
|
+
const results = await modelWithHooks.applyAutoFlows(inputArgs);
|
|
642
|
+
|
|
643
|
+
// Verify hooks were called
|
|
644
|
+
expect(beforeHookSpy).toHaveBeenCalledTimes(1);
|
|
645
|
+
expect(afterHookSpy).toHaveBeenCalledTimes(1);
|
|
646
|
+
expect(errorHookSpy).not.toHaveBeenCalled();
|
|
647
|
+
|
|
648
|
+
// Verify hook parameters
|
|
649
|
+
expect(beforeHookSpy).toHaveBeenCalledWith(inputArgs);
|
|
650
|
+
|
|
651
|
+
expect(afterHookSpy).toHaveBeenCalledWith(
|
|
652
|
+
expect.arrayContaining([expect.objectContaining({ autoStep: 'auto-result' })]),
|
|
653
|
+
inputArgs,
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test('should allow onBeforeAutoFlows to terminate flow via ctx.exit()', async () => {
|
|
658
|
+
const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1' };
|
|
659
|
+
const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2' };
|
|
660
|
+
|
|
661
|
+
const TestFlowModelWithExitHooks = class extends TestFlowModel {
|
|
662
|
+
async onBeforeAutoFlows(inputArgs?: Record<string, any>) {
|
|
663
|
+
beforeHookSpy(inputArgs);
|
|
664
|
+
throw new FlowExitException('autoFlows', this.uid);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async onAfterAutoFlows(results: any[], inputArgs?: Record<string, any>) {
|
|
668
|
+
afterHookSpy(results, inputArgs);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async onAutoFlowsError(error: Error, inputArgs?: Record<string, any>) {
|
|
672
|
+
errorHookSpy(error, inputArgs);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// 在正确的类上注册流程
|
|
677
|
+
TestFlowModelWithExitHooks.registerFlow(autoFlow1);
|
|
678
|
+
TestFlowModelWithExitHooks.registerFlow(autoFlow2);
|
|
679
|
+
|
|
680
|
+
const modelWithHooks = new TestFlowModelWithExitHooks(modelOptions);
|
|
681
|
+
const results = await modelWithHooks.applyAutoFlows();
|
|
682
|
+
|
|
683
|
+
// Should have called onBeforeAutoFlows but not onAfterAutoFlows
|
|
684
|
+
expect(beforeHookSpy).toHaveBeenCalledTimes(1);
|
|
685
|
+
expect(afterHookSpy).not.toHaveBeenCalled();
|
|
686
|
+
expect(errorHookSpy).not.toHaveBeenCalled();
|
|
687
|
+
|
|
688
|
+
// Should return empty results since flow was terminated early
|
|
689
|
+
expect(results).toEqual([]);
|
|
690
|
+
|
|
691
|
+
// Auto flows should not have been executed
|
|
692
|
+
expect(autoFlow1.steps.autoStep.handler).not.toHaveBeenCalled();
|
|
693
|
+
expect(autoFlow2.steps.autoStep.handler).not.toHaveBeenCalled();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('should call onAutoFlowsError when flow execution fails', async () => {
|
|
697
|
+
const errorFlow = {
|
|
698
|
+
key: 'errorFlow',
|
|
699
|
+
|
|
700
|
+
steps: {
|
|
701
|
+
errorStep: {
|
|
702
|
+
handler: vi.fn().mockImplementation(() => {
|
|
703
|
+
throw new Error('Test error');
|
|
704
|
+
}),
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
TestFlowModelWithHooks.registerFlow(errorFlow);
|
|
709
|
+
|
|
710
|
+
const modelWithHooks = new TestFlowModelWithHooks(modelOptions);
|
|
711
|
+
|
|
712
|
+
// 测试错误处理钩子功能
|
|
713
|
+
await expect(modelWithHooks.applyAutoFlows()).rejects.toThrow('Test error');
|
|
714
|
+
|
|
715
|
+
// Verify hooks were called
|
|
716
|
+
expect(beforeHookSpy).toHaveBeenCalledTimes(1);
|
|
717
|
+
expect(afterHookSpy).not.toHaveBeenCalled();
|
|
718
|
+
expect(errorHookSpy).toHaveBeenCalledTimes(1);
|
|
719
|
+
|
|
720
|
+
// Verify error hook parameters
|
|
721
|
+
expect(errorHookSpy).toHaveBeenCalledWith(
|
|
722
|
+
expect.objectContaining({
|
|
723
|
+
message: 'Test error',
|
|
724
|
+
}),
|
|
725
|
+
undefined, // inputArgs was not provided
|
|
726
|
+
);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('should provide access to step results in onAfterAutoFlows', async () => {
|
|
730
|
+
const autoFlow1 = { ...createAutoFlowDefinition(), key: 'auto1' };
|
|
731
|
+
const autoFlow2 = { ...createAutoFlowDefinition(), key: 'auto2' };
|
|
732
|
+
TestFlowModelWithHooks.registerFlow(autoFlow1);
|
|
733
|
+
TestFlowModelWithHooks.registerFlow(autoFlow2);
|
|
734
|
+
|
|
735
|
+
const modelWithHooks = new TestFlowModelWithHooks(modelOptions);
|
|
736
|
+
await modelWithHooks.applyAutoFlows();
|
|
737
|
+
|
|
738
|
+
expect(afterHookSpy).toHaveBeenCalledTimes(1);
|
|
739
|
+
|
|
740
|
+
const [results, inputArgs] = afterHookSpy.mock.calls[0];
|
|
741
|
+
|
|
742
|
+
// Verify results array contains results from both flows
|
|
743
|
+
expect(results).toHaveLength(2);
|
|
744
|
+
expect(results[0]).toEqual({ autoStep: 'auto-result' });
|
|
745
|
+
expect(results[1]).toEqual({ autoStep: 'auto-result' });
|
|
746
|
+
|
|
747
|
+
// Verify inputArgs is undefined since none was provided
|
|
748
|
+
expect(inputArgs).toBeUndefined();
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
describe('dispatchEvent', () => {
|
|
754
|
+
test('should execute event-triggered flows', async () => {
|
|
755
|
+
const eventFlow = createEventFlowDefinition('testEvent');
|
|
756
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
757
|
+
|
|
758
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
model.dispatchEvent('testEvent', { data: 'payload' });
|
|
762
|
+
|
|
763
|
+
// Use a more reliable approach than arbitrary timeout
|
|
764
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
765
|
+
|
|
766
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
767
|
+
expect.stringContaining('[FlowModel] dispatchEvent: uid=test-model-uid, event=testEvent'),
|
|
768
|
+
);
|
|
769
|
+
expect(eventFlow.steps.eventStep.handler).toHaveBeenCalledWith(
|
|
770
|
+
expect.objectContaining({
|
|
771
|
+
inputArgs: { data: 'payload' },
|
|
772
|
+
}),
|
|
773
|
+
expect.any(Object),
|
|
774
|
+
);
|
|
775
|
+
} finally {
|
|
776
|
+
consoleSpy.mockRestore();
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test('should handle multiple flows for same event', async () => {
|
|
781
|
+
const eventFlow1 = { ...createEventFlowDefinition('sharedEvent'), key: 'event1' };
|
|
782
|
+
const eventFlow2 = { ...createEventFlowDefinition('sharedEvent'), key: 'event2' };
|
|
783
|
+
|
|
784
|
+
TestFlowModel.registerFlow(eventFlow1);
|
|
785
|
+
TestFlowModel.registerFlow(eventFlow2);
|
|
786
|
+
|
|
787
|
+
model.dispatchEvent('sharedEvent');
|
|
788
|
+
|
|
789
|
+
// Use a more reliable approach than arbitrary timeout
|
|
790
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
791
|
+
|
|
792
|
+
expect(eventFlow1.steps.eventStep.handler).toHaveBeenCalled();
|
|
793
|
+
expect(eventFlow2.steps.eventStep.handler).toHaveBeenCalled();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe('debounce functionality', () => {
|
|
797
|
+
test('should use debounced dispatch when debounce option is true', async () => {
|
|
798
|
+
const eventFlow = createEventFlowDefinition('debouncedEvent');
|
|
799
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
800
|
+
|
|
801
|
+
const _dispatchEventSpy = vi.spyOn(model as any, '_dispatchEvent');
|
|
802
|
+
const _dispatchEventWithDebounceSpy = vi.spyOn(model as any, '_dispatchEventWithDebounce');
|
|
803
|
+
|
|
804
|
+
// Test with debounce enabled
|
|
805
|
+
await model.dispatchEvent('debouncedEvent', { data: 'test' }, { debounce: true });
|
|
806
|
+
|
|
807
|
+
expect(_dispatchEventWithDebounceSpy).toHaveBeenCalledWith('debouncedEvent', { data: 'test' });
|
|
808
|
+
expect(_dispatchEventSpy).not.toHaveBeenCalled();
|
|
809
|
+
|
|
810
|
+
_dispatchEventSpy.mockRestore();
|
|
811
|
+
_dispatchEventWithDebounceSpy.mockRestore();
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test('should use normal dispatch when debounce option is false', async () => {
|
|
815
|
+
const eventFlow = createEventFlowDefinition('normalEvent');
|
|
816
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
817
|
+
|
|
818
|
+
const _dispatchEventSpy = vi.spyOn(model as any, '_dispatchEvent');
|
|
819
|
+
const _dispatchEventWithDebounceSpy = vi.spyOn(model as any, '_dispatchEventWithDebounce');
|
|
820
|
+
|
|
821
|
+
// Test with debounce disabled
|
|
822
|
+
await model.dispatchEvent('normalEvent', { data: 'test' }, { debounce: false });
|
|
823
|
+
|
|
824
|
+
expect(_dispatchEventSpy).toHaveBeenCalledWith('normalEvent', { data: 'test' });
|
|
825
|
+
expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
|
|
826
|
+
|
|
827
|
+
_dispatchEventSpy.mockRestore();
|
|
828
|
+
_dispatchEventWithDebounceSpy.mockRestore();
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test('should use normal dispatch when debounce option is not provided', async () => {
|
|
832
|
+
const eventFlow = createEventFlowDefinition('defaultEvent');
|
|
833
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
834
|
+
|
|
835
|
+
const _dispatchEventSpy = vi.spyOn(model as any, '_dispatchEvent');
|
|
836
|
+
const _dispatchEventWithDebounceSpy = vi.spyOn(model as any, '_dispatchEventWithDebounce');
|
|
837
|
+
|
|
838
|
+
// Test without debounce option
|
|
839
|
+
await model.dispatchEvent('defaultEvent', { data: 'test' });
|
|
840
|
+
|
|
841
|
+
expect(_dispatchEventSpy).toHaveBeenCalledWith('defaultEvent', { data: 'test' });
|
|
842
|
+
expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
|
|
843
|
+
|
|
844
|
+
_dispatchEventSpy.mockRestore();
|
|
845
|
+
_dispatchEventWithDebounceSpy.mockRestore();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test('should use normal dispatch when options is undefined', async () => {
|
|
849
|
+
const eventFlow = createEventFlowDefinition('undefinedOptionsEvent');
|
|
850
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
851
|
+
|
|
852
|
+
const _dispatchEventSpy = vi.spyOn(model as any, '_dispatchEvent');
|
|
853
|
+
const _dispatchEventWithDebounceSpy = vi.spyOn(model as any, '_dispatchEventWithDebounce');
|
|
854
|
+
|
|
855
|
+
// Test with undefined options
|
|
856
|
+
await model.dispatchEvent('undefinedOptionsEvent', { data: 'test' }, undefined);
|
|
857
|
+
|
|
858
|
+
expect(_dispatchEventSpy).toHaveBeenCalledWith('undefinedOptionsEvent', { data: 'test' });
|
|
859
|
+
expect(_dispatchEventWithDebounceSpy).not.toHaveBeenCalled();
|
|
860
|
+
|
|
861
|
+
_dispatchEventSpy.mockRestore();
|
|
862
|
+
_dispatchEventWithDebounceSpy.mockRestore();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test('should debounce multiple rapid calls when debounce is true', async () => {
|
|
866
|
+
const eventFlow = createEventFlowDefinition('rapidEvent');
|
|
867
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
868
|
+
|
|
869
|
+
const handlerSpy = eventFlow.steps.eventStep.handler as any;
|
|
870
|
+
handlerSpy.mockClear();
|
|
871
|
+
|
|
872
|
+
// Make multiple rapid calls with debounce enabled
|
|
873
|
+
model.dispatchEvent('rapidEvent', { call: 1 }, { debounce: true });
|
|
874
|
+
model.dispatchEvent('rapidEvent', { call: 2 }, { debounce: true });
|
|
875
|
+
model.dispatchEvent('rapidEvent', { call: 3 }, { debounce: true });
|
|
876
|
+
|
|
877
|
+
// Wait for debounce timeout (100ms + buffer)
|
|
878
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
879
|
+
|
|
880
|
+
// Only the last call should be executed due to debouncing
|
|
881
|
+
expect(handlerSpy).toHaveBeenCalledTimes(1);
|
|
882
|
+
expect(handlerSpy).toHaveBeenLastCalledWith(
|
|
883
|
+
expect.objectContaining({
|
|
884
|
+
inputArgs: { call: 3 },
|
|
885
|
+
}),
|
|
886
|
+
expect.any(Object),
|
|
887
|
+
);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test('should not debounce calls when debounce is false', async () => {
|
|
891
|
+
const eventFlow = createEventFlowDefinition('nonDebouncedEvent');
|
|
892
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
893
|
+
|
|
894
|
+
const handlerSpy = eventFlow.steps.eventStep.handler as any;
|
|
895
|
+
handlerSpy.mockClear();
|
|
896
|
+
|
|
897
|
+
// Make multiple rapid calls with debounce disabled
|
|
898
|
+
await model.dispatchEvent('nonDebouncedEvent', { call: 1 }, { debounce: false });
|
|
899
|
+
await model.dispatchEvent('nonDebouncedEvent', { call: 2 }, { debounce: false });
|
|
900
|
+
await model.dispatchEvent('nonDebouncedEvent', { call: 3 }, { debounce: false });
|
|
901
|
+
|
|
902
|
+
// All calls should be executed
|
|
903
|
+
expect(handlerSpy).toHaveBeenCalledTimes(3);
|
|
904
|
+
expect(handlerSpy).toHaveBeenNthCalledWith(
|
|
905
|
+
1,
|
|
906
|
+
expect.objectContaining({
|
|
907
|
+
inputArgs: { call: 1 },
|
|
908
|
+
}),
|
|
909
|
+
expect.any(Object),
|
|
910
|
+
);
|
|
911
|
+
expect(handlerSpy).toHaveBeenNthCalledWith(
|
|
912
|
+
2,
|
|
913
|
+
expect.objectContaining({
|
|
914
|
+
inputArgs: { call: 2 },
|
|
915
|
+
}),
|
|
916
|
+
expect.any(Object),
|
|
917
|
+
);
|
|
918
|
+
expect(handlerSpy).toHaveBeenNthCalledWith(
|
|
919
|
+
3,
|
|
920
|
+
expect.objectContaining({
|
|
921
|
+
inputArgs: { call: 3 },
|
|
922
|
+
}),
|
|
923
|
+
expect.any(Object),
|
|
924
|
+
);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test('should handle mixed debounced and non-debounced calls correctly', async () => {
|
|
928
|
+
const eventFlow = createEventFlowDefinition('mixedEvent');
|
|
929
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
930
|
+
|
|
931
|
+
const handlerSpy = eventFlow.steps.eventStep.handler as any;
|
|
932
|
+
handlerSpy.mockClear();
|
|
933
|
+
|
|
934
|
+
// Make a non-debounced call
|
|
935
|
+
await model.dispatchEvent('mixedEvent', { type: 'immediate' }, { debounce: false });
|
|
936
|
+
|
|
937
|
+
// Make rapid debounced calls
|
|
938
|
+
model.dispatchEvent('mixedEvent', { type: 'debounced', call: 1 }, { debounce: true });
|
|
939
|
+
model.dispatchEvent('mixedEvent', { type: 'debounced', call: 2 }, { debounce: true });
|
|
940
|
+
|
|
941
|
+
// Wait for debounce timeout
|
|
942
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
943
|
+
|
|
944
|
+
// Should have immediate call + one debounced call
|
|
945
|
+
expect(handlerSpy).toHaveBeenCalledTimes(2);
|
|
946
|
+
expect(handlerSpy).toHaveBeenNthCalledWith(
|
|
947
|
+
1,
|
|
948
|
+
expect.objectContaining({
|
|
949
|
+
inputArgs: { type: 'immediate' },
|
|
950
|
+
}),
|
|
951
|
+
expect.any(Object),
|
|
952
|
+
);
|
|
953
|
+
expect(handlerSpy).toHaveBeenNthCalledWith(
|
|
954
|
+
2,
|
|
955
|
+
expect.objectContaining({
|
|
956
|
+
inputArgs: { type: 'debounced', call: 2 },
|
|
957
|
+
}),
|
|
958
|
+
expect.any(Object),
|
|
959
|
+
);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
test('should pass correct arguments to debounced function', async () => {
|
|
963
|
+
const eventFlow = createEventFlowDefinition('argumentsEvent');
|
|
964
|
+
TestFlowModel.registerFlow(eventFlow);
|
|
965
|
+
|
|
966
|
+
const handlerSpy = eventFlow.steps.eventStep.handler as any;
|
|
967
|
+
handlerSpy.mockClear();
|
|
968
|
+
|
|
969
|
+
const inputArgs = {
|
|
970
|
+
userId: 123,
|
|
971
|
+
action: 'click',
|
|
972
|
+
timestamp: Date.now(),
|
|
973
|
+
metadata: { source: 'test' },
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
model.dispatchEvent('argumentsEvent', inputArgs, { debounce: true });
|
|
977
|
+
|
|
978
|
+
// Wait for debounce timeout
|
|
979
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
980
|
+
|
|
981
|
+
expect(handlerSpy).toHaveBeenCalledTimes(1);
|
|
982
|
+
expect(handlerSpy).toHaveBeenCalledWith(
|
|
983
|
+
expect.objectContaining({
|
|
984
|
+
inputArgs,
|
|
985
|
+
}),
|
|
986
|
+
expect.any(Object),
|
|
987
|
+
);
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// ==================== RELATIONSHIPS ====================
|
|
994
|
+
describe('Relationships', () => {
|
|
995
|
+
let model: FlowModel;
|
|
996
|
+
|
|
997
|
+
beforeEach(() => {
|
|
998
|
+
model = new FlowModel(modelOptions);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
describe('parent-child relationships', () => {
|
|
1002
|
+
test('should set parent correctly', () => {
|
|
1003
|
+
const parent = new FlowModel({ ...modelOptions, uid: 'parent' });
|
|
1004
|
+
|
|
1005
|
+
model.setParent(parent);
|
|
1006
|
+
|
|
1007
|
+
expect(model.parent).toBe(parent);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
test('should not allow setting parent to null', () => {
|
|
1011
|
+
const parent = new FlowModel({ ...modelOptions, uid: 'parent' });
|
|
1012
|
+
|
|
1013
|
+
model.setParent(parent);
|
|
1014
|
+
expect(model.parent).toBe(parent);
|
|
1015
|
+
|
|
1016
|
+
expect(() => model.setParent(null as any)).toThrow('Parent must be an instance of FlowModel');
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
describe('subModels management', () => {
|
|
1021
|
+
let parentModel: FlowModel;
|
|
1022
|
+
|
|
1023
|
+
beforeEach(() => {
|
|
1024
|
+
parentModel = new FlowModel(modelOptions);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
describe('setSubModel (object type)', () => {
|
|
1028
|
+
test('should set single subModel with FlowModel instance', () => {
|
|
1029
|
+
const childModel = new FlowModel({
|
|
1030
|
+
uid: 'child-model-uid',
|
|
1031
|
+
flowEngine,
|
|
1032
|
+
stepParams: { childFlow: { childStep: { childParam: 'childValue' } } },
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const result = parentModel.setSubModel('testChild', childModel);
|
|
1036
|
+
|
|
1037
|
+
expect(result.uid).toBe(childModel.uid);
|
|
1038
|
+
expect(result.parent).toBe(parentModel);
|
|
1039
|
+
expect((parentModel.subModels.testChild as FlowModel).uid).toBe(result.uid);
|
|
1040
|
+
expect(result.uid).toBe('child-model-uid');
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test('should replace existing subModel', () => {
|
|
1044
|
+
const firstChild = new FlowModel({ uid: 'first-child', flowEngine });
|
|
1045
|
+
const secondChild = new FlowModel({ uid: 'second-child', flowEngine });
|
|
1046
|
+
|
|
1047
|
+
parentModel.setSubModel('testChild', firstChild);
|
|
1048
|
+
const result = parentModel.setSubModel('testChild', secondChild);
|
|
1049
|
+
|
|
1050
|
+
expect(result.uid).toBe(secondChild.uid);
|
|
1051
|
+
expect((parentModel.subModels.testChild as FlowModel).uid).toBe(result.uid);
|
|
1052
|
+
expect(result.uid).toBe('second-child');
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test('should throw error when setting model with existing parent', () => {
|
|
1056
|
+
const childModel = new FlowModel({ uid: 'child-with-parent', flowEngine });
|
|
1057
|
+
const otherParent = new FlowModel({ uid: 'other-parent', flowEngine });
|
|
1058
|
+
childModel.setParent(otherParent);
|
|
1059
|
+
|
|
1060
|
+
expect(() => {
|
|
1061
|
+
parentModel.setSubModel('testChild', childModel);
|
|
1062
|
+
}).toThrow('Sub model already has a parent.');
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
test('should emit onSubModelAdded event', () => {
|
|
1066
|
+
const eventSpy = vi.fn();
|
|
1067
|
+
parentModel.emitter.on('onSubModelAdded', eventSpy);
|
|
1068
|
+
const childModel = new FlowModel({ uid: 'test-child', flowEngine });
|
|
1069
|
+
|
|
1070
|
+
const result = parentModel.setSubModel('testChild', childModel);
|
|
1071
|
+
|
|
1072
|
+
expect(eventSpy).toHaveBeenCalledWith(result);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
test('should allow setSubModel via fork and bind to master', () => {
|
|
1076
|
+
const childModel = new FlowModel({ uid: 'object-child-via-fork', flowEngine });
|
|
1077
|
+
const fork = parentModel.createFork();
|
|
1078
|
+
|
|
1079
|
+
const result = (fork as any).setSubModel('testChildObject', childModel);
|
|
1080
|
+
|
|
1081
|
+
expect(result.parent).toBe(parentModel);
|
|
1082
|
+
expect((parentModel.subModels as any)['testChildObject']).toBe(result);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test('should allow multiple setSubModel via fork with same instance without error', () => {
|
|
1086
|
+
const childModel = new FlowModel({ uid: 'object-child-via-fork-2', flowEngine });
|
|
1087
|
+
const fork = parentModel.createFork();
|
|
1088
|
+
|
|
1089
|
+
const first = (fork as any).setSubModel('testChildObject2', childModel);
|
|
1090
|
+
const second = (fork as any).setSubModel('testChildObject2', childModel);
|
|
1091
|
+
|
|
1092
|
+
expect(first).toBe(second);
|
|
1093
|
+
expect(second.parent).toBe(parentModel);
|
|
1094
|
+
expect((parentModel.subModels as any)['testChildObject2']).toBe(second);
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
describe('addSubModel (array type)', () => {
|
|
1099
|
+
test('should add subModel to array with FlowModel instance', () => {
|
|
1100
|
+
const childModel = new FlowModel({
|
|
1101
|
+
uid: 'child-model-uid',
|
|
1102
|
+
flowEngine,
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
const result = parentModel.addSubModel('testChildren', childModel);
|
|
1106
|
+
|
|
1107
|
+
expect(result.uid).toBe(childModel.uid);
|
|
1108
|
+
expect(result.parent).toBe(parentModel);
|
|
1109
|
+
expect(Array.isArray(parentModel.subModels.testChildren)).toBe(true);
|
|
1110
|
+
expect((parentModel.subModels.testChildren as FlowModel[]).some((model) => model.uid === result.uid)).toBe(
|
|
1111
|
+
true,
|
|
1112
|
+
);
|
|
1113
|
+
expect(result.sortIndex).toBe(1);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
test('should add multiple subModels with correct sortIndex', () => {
|
|
1117
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1118
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1119
|
+
const child3 = new FlowModel({ uid: 'child3', flowEngine });
|
|
1120
|
+
|
|
1121
|
+
parentModel.addSubModel('testChildren', child1);
|
|
1122
|
+
parentModel.addSubModel('testChildren', child2);
|
|
1123
|
+
parentModel.addSubModel('testChildren', child3);
|
|
1124
|
+
|
|
1125
|
+
expect(child1.sortIndex).toBe(1);
|
|
1126
|
+
expect(child2.sortIndex).toBe(2);
|
|
1127
|
+
expect(child3.sortIndex).toBe(3);
|
|
1128
|
+
expect(parentModel.subModels.testChildren).toHaveLength(3);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
test('should maintain sortIndex when adding to existing array', () => {
|
|
1132
|
+
const existingChild = new FlowModel({ uid: 'existing', flowEngine, sortIndex: 5 });
|
|
1133
|
+
(parentModel.subModels as any).testChildren = [existingChild];
|
|
1134
|
+
|
|
1135
|
+
const newChild = new FlowModel({ uid: 'new-child', flowEngine });
|
|
1136
|
+
parentModel.addSubModel('testChildren', newChild);
|
|
1137
|
+
|
|
1138
|
+
expect(newChild.sortIndex).toBe(6); // Should be max(5) + 1
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test('should throw error when adding model with existing parent', () => {
|
|
1142
|
+
const childModel = new FlowModel({ uid: 'child-with-parent', flowEngine });
|
|
1143
|
+
const otherParent = new FlowModel({ uid: 'other-parent', flowEngine });
|
|
1144
|
+
childModel.setParent(otherParent);
|
|
1145
|
+
|
|
1146
|
+
expect(() => {
|
|
1147
|
+
parentModel.addSubModel('testChildren', childModel);
|
|
1148
|
+
}).toThrow('Sub model already has a parent.');
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
test('should emit onSubModelAdded event', () => {
|
|
1152
|
+
const eventSpy = vi.fn();
|
|
1153
|
+
parentModel.emitter.on('onSubModelAdded', eventSpy);
|
|
1154
|
+
const childModel = new FlowModel({ uid: 'test-child', flowEngine });
|
|
1155
|
+
|
|
1156
|
+
const result = parentModel.addSubModel('testChildren', childModel);
|
|
1157
|
+
|
|
1158
|
+
expect(eventSpy).toHaveBeenCalledWith(result);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
test('should allow addSubModel via fork and bind to master', () => {
|
|
1162
|
+
const childModel = new FlowModel({ uid: 'child-via-fork', flowEngine });
|
|
1163
|
+
const fork = parentModel.createFork();
|
|
1164
|
+
|
|
1165
|
+
const result = (fork as any).addSubModel('testChildren', childModel);
|
|
1166
|
+
|
|
1167
|
+
expect(result.parent).toBe(parentModel);
|
|
1168
|
+
expect(Array.isArray(parentModel.subModels.testChildren)).toBe(true);
|
|
1169
|
+
expect((parentModel.subModels.testChildren as FlowModel[]).some((m) => m.uid === 'child-via-fork')).toBe(
|
|
1170
|
+
true,
|
|
1171
|
+
);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test('should allow multiple addSubModel via fork with same instance without error', () => {
|
|
1175
|
+
const childModel = new FlowModel({ uid: 'child-via-fork-2', flowEngine });
|
|
1176
|
+
const fork = parentModel.createFork();
|
|
1177
|
+
|
|
1178
|
+
const r1 = (fork as any).addSubModel('testChildren2', childModel);
|
|
1179
|
+
const r2 = (fork as any).addSubModel('testChildren2', childModel);
|
|
1180
|
+
|
|
1181
|
+
expect(r1).toBe(r2);
|
|
1182
|
+
expect(r1.parent).toBe(parentModel);
|
|
1183
|
+
const arr = (parentModel.subModels as any)['testChildren2'];
|
|
1184
|
+
expect(Array.isArray(arr)).toBe(true);
|
|
1185
|
+
// allow duplicate binding without throwing
|
|
1186
|
+
expect(arr.length).toBe(2);
|
|
1187
|
+
expect(arr[0]).toBe(childModel);
|
|
1188
|
+
expect(arr[1]).toBe(childModel);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
describe('mapSubModels', () => {
|
|
1193
|
+
test('should map over array subModels', () => {
|
|
1194
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1195
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1196
|
+
|
|
1197
|
+
parentModel.addSubModel('testChildren', child1);
|
|
1198
|
+
parentModel.addSubModel('testChildren', child2);
|
|
1199
|
+
|
|
1200
|
+
const results = parentModel.mapSubModels('testChildren', (model, index) => ({
|
|
1201
|
+
uid: model.uid,
|
|
1202
|
+
index,
|
|
1203
|
+
}));
|
|
1204
|
+
|
|
1205
|
+
expect(results).toHaveLength(2);
|
|
1206
|
+
expect(results[0]).toEqual({ uid: 'child1', index: 0 });
|
|
1207
|
+
expect(results[1]).toEqual({ uid: 'child2', index: 1 });
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
test('should map over single subModel', () => {
|
|
1211
|
+
const child = new FlowModel({ uid: 'single-child', flowEngine });
|
|
1212
|
+
parentModel.setSubModel('testChild', child);
|
|
1213
|
+
|
|
1214
|
+
const results = parentModel.mapSubModels('testChild', (model, index) => ({
|
|
1215
|
+
uid: model.uid,
|
|
1216
|
+
index,
|
|
1217
|
+
}));
|
|
1218
|
+
|
|
1219
|
+
expect(results).toHaveLength(1);
|
|
1220
|
+
expect(results[0]).toEqual({ uid: 'single-child', index: 0 });
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
test('should return empty array for non-existent subModel', () => {
|
|
1224
|
+
const results = parentModel.mapSubModels('nonExistent', (model) => model.uid);
|
|
1225
|
+
|
|
1226
|
+
expect(results).toEqual([]);
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
describe('findSubModel', () => {
|
|
1231
|
+
test('should find subModel by condition in array', () => {
|
|
1232
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1233
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1234
|
+
|
|
1235
|
+
parentModel.addSubModel('testChildren', child1);
|
|
1236
|
+
parentModel.addSubModel('testChildren', child2);
|
|
1237
|
+
|
|
1238
|
+
const found = parentModel.findSubModel('testChildren', (model) => model.uid === 'child2');
|
|
1239
|
+
|
|
1240
|
+
expect(found).toBeDefined();
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
test('should find single subModel by condition', () => {
|
|
1244
|
+
const child = new FlowModel({ uid: 'target-child', flowEngine });
|
|
1245
|
+
parentModel.setSubModel('testChild', child);
|
|
1246
|
+
|
|
1247
|
+
const found = parentModel.findSubModel('testChild', (model) => model.uid === 'target-child');
|
|
1248
|
+
|
|
1249
|
+
expect(found).toBeDefined();
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
test('should return null when no match found', () => {
|
|
1253
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1254
|
+
parentModel.addSubModel('testChildren', child1);
|
|
1255
|
+
|
|
1256
|
+
const found = parentModel.findSubModel('testChildren', (model) => model.uid === 'nonexistent');
|
|
1257
|
+
|
|
1258
|
+
expect(found).toBeNull();
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
test('should return null for non-existent subModel key', () => {
|
|
1262
|
+
const found = parentModel.findSubModel('nonExistent', () => true);
|
|
1263
|
+
|
|
1264
|
+
expect(found).toBeNull();
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
describe('applySubModelsAutoFlows', () => {
|
|
1269
|
+
test('should apply auto flows to all array subModels', async () => {
|
|
1270
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1271
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1272
|
+
|
|
1273
|
+
child1.applyAutoFlows = vi.fn().mockResolvedValue([]);
|
|
1274
|
+
child2.applyAutoFlows = vi.fn().mockResolvedValue([]);
|
|
1275
|
+
|
|
1276
|
+
parentModel.addSubModel('children', child1);
|
|
1277
|
+
parentModel.addSubModel('children', child2);
|
|
1278
|
+
|
|
1279
|
+
const runtimeData = { test: 'extra' };
|
|
1280
|
+
|
|
1281
|
+
await parentModel.applySubModelsAutoFlows('children', runtimeData);
|
|
1282
|
+
|
|
1283
|
+
expect(child1.applyAutoFlows).toHaveBeenCalledWith(runtimeData);
|
|
1284
|
+
expect(child2.applyAutoFlows).toHaveBeenCalledWith(runtimeData);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test('should apply auto flows to single subModel', async () => {
|
|
1288
|
+
const child = new FlowModel({ uid: 'child', flowEngine });
|
|
1289
|
+
|
|
1290
|
+
child.applyAutoFlows = vi.fn().mockResolvedValue([]);
|
|
1291
|
+
|
|
1292
|
+
parentModel.setSubModel('child', child);
|
|
1293
|
+
|
|
1294
|
+
const runtimeData = { test: 'extra' };
|
|
1295
|
+
|
|
1296
|
+
await parentModel.applySubModelsAutoFlows('child', runtimeData);
|
|
1297
|
+
|
|
1298
|
+
expect(child.applyAutoFlows).toHaveBeenCalledWith(runtimeData);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
test('should handle empty subModels gracefully', async () => {
|
|
1302
|
+
await expect(parentModel.applySubModelsAutoFlows('nonExistent')).resolves.not.toThrow();
|
|
1303
|
+
});
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
describe('subModels serialization', () => {
|
|
1307
|
+
test('should serialize subModels in model data', () => {
|
|
1308
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1309
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1310
|
+
|
|
1311
|
+
parentModel.setSubModel('singleChild', child1);
|
|
1312
|
+
parentModel.addSubModel('multipleChildren', child2);
|
|
1313
|
+
|
|
1314
|
+
const serialized = parentModel.serialize();
|
|
1315
|
+
|
|
1316
|
+
expect(serialized.subModels).toBeDefined();
|
|
1317
|
+
expect(serialized.subModels.singleChild).toBeDefined();
|
|
1318
|
+
expect(serialized.subModels.multipleChildren).toBeDefined();
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
test('should handle empty subModels in serialization', () => {
|
|
1322
|
+
const serialized = parentModel.serialize();
|
|
1323
|
+
|
|
1324
|
+
expect(serialized.subModels).toBeDefined();
|
|
1325
|
+
expect(typeof serialized.subModels).toBe('object');
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
describe('subModels reactive behavior', () => {
|
|
1330
|
+
test('should trigger reactive updates when subModels change', () => {
|
|
1331
|
+
const child = new FlowModel({ uid: 'reactive-child', flowEngine });
|
|
1332
|
+
let reactionTriggered = false;
|
|
1333
|
+
|
|
1334
|
+
// Mock a simple reaction to observe changes
|
|
1335
|
+
const observer = () => {
|
|
1336
|
+
reactionTriggered = true;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// Observe changes to subModels
|
|
1340
|
+
parentModel.on('subModelChanged', observer);
|
|
1341
|
+
|
|
1342
|
+
// Add a subModel and verify props are reactive
|
|
1343
|
+
parentModel.setSubModel('reactiveTest', child);
|
|
1344
|
+
|
|
1345
|
+
// Test that the subModel was added
|
|
1346
|
+
expect(parentModel.subModels.reactiveTest).toBeDefined();
|
|
1347
|
+
expect((parentModel.subModels.reactiveTest as FlowModel).uid).toBe('reactive-child');
|
|
1348
|
+
|
|
1349
|
+
// Test that props are observable
|
|
1350
|
+
child.setProps({ reactiveTest: 'initialValue' });
|
|
1351
|
+
expect(child.props.reactiveTest).toBe('initialValue');
|
|
1352
|
+
|
|
1353
|
+
// Change props and verify it's reactive
|
|
1354
|
+
child.setProps({ reactiveTest: 'updatedValue' });
|
|
1355
|
+
expect(child.props.reactiveTest).toBe('updatedValue');
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
test('should maintain reactive stepParams', () => {
|
|
1359
|
+
const child = new FlowModel({ uid: 'step-params-child', flowEngine });
|
|
1360
|
+
parentModel.setSubModel('stepParamsTest', child);
|
|
1361
|
+
|
|
1362
|
+
// Set initial step params
|
|
1363
|
+
child.setStepParams({ testFlow: { testStep: { param1: 'initial' } } });
|
|
1364
|
+
expect(child.stepParams.testFlow.testStep.param1).toBe('initial');
|
|
1365
|
+
|
|
1366
|
+
// Update step params and verify reactivity
|
|
1367
|
+
child.setStepParams({ testFlow: { testStep: { param1: 'updated', param2: 'new' } } });
|
|
1368
|
+
expect(child.stepParams.testFlow.testStep.param1).toBe('updated');
|
|
1369
|
+
expect(child.stepParams.testFlow.testStep.param2).toBe('new');
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
describe('subModels edge cases', () => {
|
|
1374
|
+
test('should handle null parent gracefully', () => {
|
|
1375
|
+
const child = new FlowModel({ uid: 'orphan-child', flowEngine });
|
|
1376
|
+
|
|
1377
|
+
expect(() => {
|
|
1378
|
+
parentModel.setSubModel('testChild', child);
|
|
1379
|
+
}).not.toThrow();
|
|
1380
|
+
|
|
1381
|
+
expect(child.parent).toBe(parentModel);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test('should handle setting subModel with same key multiple times', () => {
|
|
1385
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1386
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1387
|
+
|
|
1388
|
+
parentModel.setSubModel('sameKey', child1);
|
|
1389
|
+
parentModel.setSubModel('sameKey', child2);
|
|
1390
|
+
|
|
1391
|
+
expect((parentModel.subModels.sameKey as FlowModel).uid).toBe(child2.uid);
|
|
1392
|
+
expect((parentModel.subModels.sameKey as FlowModel).uid).toBe('child2');
|
|
1393
|
+
expect(child2.parent).toBe(parentModel);
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
test('should handle adding subModels to different arrays', () => {
|
|
1397
|
+
const child1 = new FlowModel({ uid: 'child1', flowEngine });
|
|
1398
|
+
const child2 = new FlowModel({ uid: 'child2', flowEngine });
|
|
1399
|
+
|
|
1400
|
+
parentModel.addSubModel('arrayA', child1);
|
|
1401
|
+
parentModel.addSubModel('arrayB', child2);
|
|
1402
|
+
|
|
1403
|
+
expect(Array.isArray(parentModel.subModels.arrayA)).toBe(true);
|
|
1404
|
+
expect(Array.isArray(parentModel.subModels.arrayB)).toBe(true);
|
|
1405
|
+
expect((parentModel.subModels.arrayA as FlowModel[]).some((model) => model.uid === child1.uid)).toBe(true);
|
|
1406
|
+
expect((parentModel.subModels.arrayB as FlowModel[]).some((model) => model.uid === child2.uid)).toBe(true);
|
|
1407
|
+
expect((parentModel.subModels.arrayA as FlowModel[]).some((model) => model.uid === child2.uid)).toBe(false);
|
|
1408
|
+
expect((parentModel.subModels.arrayB as FlowModel[]).some((model) => model.uid === child1.uid)).toBe(false);
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
describe('fork management', () => {
|
|
1414
|
+
test('should create fork with unique forkId', () => {
|
|
1415
|
+
const fork1 = model.createFork();
|
|
1416
|
+
const fork2 = model.createFork();
|
|
1417
|
+
|
|
1418
|
+
expect(fork1.forkId).toBe(0);
|
|
1419
|
+
expect(fork2.forkId).toBe(1);
|
|
1420
|
+
expect(model.forks.size).toBe(2);
|
|
1421
|
+
expect(model.forks.has(fork1)).toBe(true);
|
|
1422
|
+
expect(model.forks.has(fork2)).toBe(true);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
test('should cache fork instances with key', () => {
|
|
1426
|
+
const fork1 = model.createFork({}, 'cacheKey');
|
|
1427
|
+
const fork2 = model.createFork({}, 'cacheKey'); // Same key should return cached instance
|
|
1428
|
+
|
|
1429
|
+
expect(fork1).toBe(fork2);
|
|
1430
|
+
expect(model.forks.size).toBe(1);
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
test('should create different instances for different keys', () => {
|
|
1434
|
+
const fork1 = model.createFork({}, 'key1');
|
|
1435
|
+
const fork2 = model.createFork({}, 'key2');
|
|
1436
|
+
|
|
1437
|
+
expect(fork1).not.toBe(fork2);
|
|
1438
|
+
expect(fork1.forkId).toBe(0);
|
|
1439
|
+
expect(fork2.forkId).toBe(1);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
test('should create fork with local props', () => {
|
|
1443
|
+
const localProps = { name: 'Local Fork', value: 42 };
|
|
1444
|
+
const fork = model.createFork(localProps);
|
|
1445
|
+
|
|
1446
|
+
expect(fork.localProps).toEqual(localProps);
|
|
1447
|
+
expect(fork['master']).toBe(model);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
test('should dispose all forks when clearing', () => {
|
|
1451
|
+
const fork1 = model.createFork();
|
|
1452
|
+
const fork2 = model.createFork();
|
|
1453
|
+
|
|
1454
|
+
// 手动 mock dispose 方法为 spy
|
|
1455
|
+
fork1.dispose = vi.fn();
|
|
1456
|
+
fork2.dispose = vi.fn();
|
|
1457
|
+
|
|
1458
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1459
|
+
|
|
1460
|
+
try {
|
|
1461
|
+
model.clearForks();
|
|
1462
|
+
|
|
1463
|
+
expect(fork1.dispose).toHaveBeenCalled();
|
|
1464
|
+
expect(fork2.dispose).toHaveBeenCalled();
|
|
1465
|
+
expect(model.forks.size).toBe(0);
|
|
1466
|
+
} finally {
|
|
1467
|
+
consoleSpy.mockRestore();
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
test('should handle empty forks collection when clearing', () => {
|
|
1472
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1473
|
+
|
|
1474
|
+
try {
|
|
1475
|
+
model.clearForks();
|
|
1476
|
+
|
|
1477
|
+
expect(model.forks.size).toBe(0);
|
|
1478
|
+
} finally {
|
|
1479
|
+
consoleSpy.mockRestore();
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
// ==================== LIFECYCLE ====================
|
|
1486
|
+
describe('Lifecycle', () => {
|
|
1487
|
+
let model: FlowModel;
|
|
1488
|
+
|
|
1489
|
+
beforeEach(() => {
|
|
1490
|
+
model = new FlowModel(modelOptions);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
describe('Component Lifecycle Hooks', () => {
|
|
1494
|
+
test('should call onMount and onUnmount with FlowModelRenderer', () => {
|
|
1495
|
+
const mountSpy = vi.fn();
|
|
1496
|
+
const unmountSpy = vi.fn();
|
|
1497
|
+
|
|
1498
|
+
class TestModel extends FlowModel {
|
|
1499
|
+
protected onMount(): void {
|
|
1500
|
+
mountSpy();
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
protected onUnmount(): void {
|
|
1504
|
+
unmountSpy();
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
public render(): any {
|
|
1508
|
+
return React.createElement('div', { 'data-testid': 'test-component' }, 'Test Component');
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const testModel = new TestModel(modelOptions);
|
|
1513
|
+
|
|
1514
|
+
// Test with FlowModelRenderer (simulated)
|
|
1515
|
+
const { unmount } = render(testModel.render());
|
|
1516
|
+
|
|
1517
|
+
// Verify onMount was called
|
|
1518
|
+
expect(mountSpy).toHaveBeenCalledTimes(1);
|
|
1519
|
+
|
|
1520
|
+
// Unmount and verify onUnmount was called
|
|
1521
|
+
unmount();
|
|
1522
|
+
expect(unmountSpy).toHaveBeenCalledTimes(1);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
test('should call onMount and onUnmount as children', () => {
|
|
1526
|
+
const mountSpy = vi.fn();
|
|
1527
|
+
const unmountSpy = vi.fn();
|
|
1528
|
+
|
|
1529
|
+
class TestModel extends FlowModel {
|
|
1530
|
+
protected onMount(): void {
|
|
1531
|
+
mountSpy();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
protected onUnmount(): void {
|
|
1535
|
+
unmountSpy();
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
public render(): any {
|
|
1539
|
+
return React.createElement('span', null, 'Child Component');
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const testModel = new TestModel(modelOptions);
|
|
1544
|
+
|
|
1545
|
+
// Test as children: <TestComponent>{model.render()}</TestComponent>
|
|
1546
|
+
const TestComponent = ({ children }: { children: React.ReactNode }) => {
|
|
1547
|
+
return React.createElement('div', { 'data-testid': 'parent' }, children);
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
const { unmount } = render(React.createElement(TestComponent, null, testModel.render()));
|
|
1551
|
+
|
|
1552
|
+
// Verify onMount was called
|
|
1553
|
+
expect(mountSpy).toHaveBeenCalledTimes(1);
|
|
1554
|
+
|
|
1555
|
+
// Unmount and verify onUnmount was called
|
|
1556
|
+
unmount();
|
|
1557
|
+
expect(unmountSpy).toHaveBeenCalledTimes(1);
|
|
1558
|
+
});
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
describe('save operations', () => {
|
|
1562
|
+
test('should save model through FlowEngine', async () => {
|
|
1563
|
+
flowEngine.saveModel = vi.fn().mockResolvedValue({ success: true, id: 'saved-id' });
|
|
1564
|
+
|
|
1565
|
+
const result = await model.save();
|
|
1566
|
+
|
|
1567
|
+
expect(flowEngine.saveModel).toHaveBeenCalledWith(model);
|
|
1568
|
+
expect(result).toEqual({ success: true, id: 'saved-id' });
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
test('should throw error when FlowEngine not set', async () => {
|
|
1572
|
+
// Since FlowModel constructor now requires flowEngine, we test the error at construction time
|
|
1573
|
+
expect(() => {
|
|
1574
|
+
new FlowModel({ uid: 'test' } as any);
|
|
1575
|
+
}).toThrow('FlowModel must be initialized with a FlowEngine instance.');
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
test('should handle save operation failures', async () => {
|
|
1579
|
+
const saveError = new Error('Save operation failed');
|
|
1580
|
+
flowEngine.saveModel = vi.fn().mockRejectedValue(saveError);
|
|
1581
|
+
|
|
1582
|
+
await expect(model.save()).rejects.toThrow('Save operation failed');
|
|
1583
|
+
expect(flowEngine.saveModel).toHaveBeenCalledWith(model);
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
describe('destruction operations', () => {
|
|
1588
|
+
test('should destroy model through FlowEngine', async () => {
|
|
1589
|
+
flowEngine.destroyModel = vi.fn().mockResolvedValue({ success: true });
|
|
1590
|
+
|
|
1591
|
+
const result = await model.destroy();
|
|
1592
|
+
|
|
1593
|
+
expect(flowEngine.destroyModel).toHaveBeenCalledWith(model.uid);
|
|
1594
|
+
expect(result).toEqual({ success: true });
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
test('should throw error when FlowEngine not available for destroy', async () => {
|
|
1598
|
+
// Since FlowModel constructor now requires flowEngine, we test the error at construction time
|
|
1599
|
+
expect(() => {
|
|
1600
|
+
new FlowModel({ uid: 'test' } as any);
|
|
1601
|
+
}).toThrow('FlowModel must be initialized with a FlowEngine instance.');
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
test('should clean up resources on remove', () => {
|
|
1605
|
+
model.createFork();
|
|
1606
|
+
model.createFork();
|
|
1607
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1608
|
+
|
|
1609
|
+
// Mock removeModel to simulate proper fork cleanup
|
|
1610
|
+
flowEngine.removeModel = vi.fn().mockImplementation(() => {
|
|
1611
|
+
model.clearForks();
|
|
1612
|
+
return true;
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
try {
|
|
1616
|
+
model.setProps({ active: true });
|
|
1617
|
+
expect(model.forks.size).toBe(2); // Verify forks were created
|
|
1618
|
+
|
|
1619
|
+
model.remove();
|
|
1620
|
+
|
|
1621
|
+
expect(model.forks.size).toBe(0);
|
|
1622
|
+
expect(flowEngine.removeModel).toHaveBeenCalledWith(model.uid);
|
|
1623
|
+
} finally {
|
|
1624
|
+
consoleSpy.mockRestore();
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
describe('rendering operations', () => {
|
|
1630
|
+
test('should not pre-call render for RenderFunction mode and call exactly once on render', () => {
|
|
1631
|
+
const renderSpy = vi.fn(() => vi.fn());
|
|
1632
|
+
|
|
1633
|
+
class CallbackRenderModel extends FlowModel {
|
|
1634
|
+
static renderMode = ModelRenderMode.RenderFunction;
|
|
1635
|
+
public render(): any {
|
|
1636
|
+
return renderSpy();
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const callbackModel = new CallbackRenderModel(modelOptions);
|
|
1641
|
+
|
|
1642
|
+
// Constructor should not trigger any render pre-call
|
|
1643
|
+
expect(renderSpy).toHaveBeenCalledTimes(0);
|
|
1644
|
+
|
|
1645
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1646
|
+
try {
|
|
1647
|
+
const { unmount } = render(
|
|
1648
|
+
React.createElement(FlowModelRenderer, { model: callbackModel, skipApplyAutoFlows: true }),
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
1652
|
+
unmount();
|
|
1653
|
+
} finally {
|
|
1654
|
+
warnSpy.mockRestore();
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
test('should not pre-call render for ReactElement mode and call exactly once on actual render', () => {
|
|
1659
|
+
const renderSpy = vi.fn(() => React.createElement('div', { 'data-testid': 'elt' }, 'Elt'));
|
|
1660
|
+
|
|
1661
|
+
class ElementRenderModel extends FlowModel {
|
|
1662
|
+
public render(): any {
|
|
1663
|
+
return renderSpy();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const elementModel = new ElementRenderModel(modelOptions);
|
|
1668
|
+
|
|
1669
|
+
// Constructor should not trigger any render pre-call
|
|
1670
|
+
expect(renderSpy).toHaveBeenCalledTimes(0);
|
|
1671
|
+
|
|
1672
|
+
const { getByTestId, unmount } = render(
|
|
1673
|
+
React.createElement(FlowModelRenderer, { model: elementModel, skipApplyAutoFlows: true }),
|
|
1674
|
+
);
|
|
1675
|
+
|
|
1676
|
+
// Render should be called exactly once during mount
|
|
1677
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
1678
|
+
expect(getByTestId('elt')).toBeTruthy();
|
|
1679
|
+
|
|
1680
|
+
unmount();
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
test('should render model to React element for default flowModel', () => {
|
|
1684
|
+
const result = model.render();
|
|
1685
|
+
|
|
1686
|
+
expect(result).toBeDefined();
|
|
1687
|
+
expect(React.isValidElement(result)).toBe(true);
|
|
1688
|
+
expect(typeof result.props).toBe('object');
|
|
1689
|
+
expect(typeof result.type).toBe('object');
|
|
1690
|
+
expect(result.type.displayName).toBe('ReactiveWrapper(FlowModel)');
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
test('should rerender with previous auto flows', async () => {
|
|
1694
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1695
|
+
model.applyAutoFlows = vi.fn().mockResolvedValue([]);
|
|
1696
|
+
|
|
1697
|
+
try {
|
|
1698
|
+
await expect(model.rerender()).resolves.not.toThrow();
|
|
1699
|
+
expect(model.applyAutoFlows).toHaveBeenCalled();
|
|
1700
|
+
} finally {
|
|
1701
|
+
consoleSpy.mockRestore();
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
describe('serialization', () => {
|
|
1707
|
+
test('should serialize basic model data, excluding props and flowEngine', () => {
|
|
1708
|
+
model.sortIndex = 5;
|
|
1709
|
+
model.setProps({ name: 'Test Model', value: 42 });
|
|
1710
|
+
model.setStepParams({
|
|
1711
|
+
flow1: { step1: { param1: 'value1' } },
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
const serialized = model.serialize();
|
|
1715
|
+
|
|
1716
|
+
expect(serialized).toEqual({
|
|
1717
|
+
uid: model.uid,
|
|
1718
|
+
stepParams: expect.objectContaining({ flow1: { step1: { param1: 'value1' } } }),
|
|
1719
|
+
sortIndex: 5,
|
|
1720
|
+
subModels: expect.any(Object),
|
|
1721
|
+
});
|
|
1722
|
+
// props should be excluded from serialization
|
|
1723
|
+
expect(serialized.props).toBeUndefined();
|
|
1724
|
+
expect(serialized.flowEngine).toBeUndefined();
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
test('should serialize empty model correctly', () => {
|
|
1728
|
+
const emptyModel = new FlowModel({
|
|
1729
|
+
uid: 'empty-model',
|
|
1730
|
+
flowEngine,
|
|
1731
|
+
stepParams: {},
|
|
1732
|
+
subModels: {},
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
emptyModel.setProps('foo', 'bar');
|
|
1736
|
+
|
|
1737
|
+
const serialized = emptyModel.serialize();
|
|
1738
|
+
|
|
1739
|
+
expect(serialized).toEqual({
|
|
1740
|
+
uid: 'empty-model',
|
|
1741
|
+
stepParams: expect.any(Object),
|
|
1742
|
+
sortIndex: expect.any(Number),
|
|
1743
|
+
subModels: expect.any(Object),
|
|
1744
|
+
});
|
|
1745
|
+
expect(serialized.flowEngine).toBeUndefined();
|
|
1746
|
+
});
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
// ==================== TITLE MANAGEMENT ====================
|
|
1751
|
+
describe('Title Management', () => {
|
|
1752
|
+
let model: FlowModel;
|
|
1753
|
+
let TestFlowModel: any;
|
|
1754
|
+
|
|
1755
|
+
beforeEach(() => {
|
|
1756
|
+
TestFlowModel = class extends FlowModel {};
|
|
1757
|
+
model = new TestFlowModel(modelOptions);
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
describe('title getter', () => {
|
|
1761
|
+
test('should return undefined when no title is set', () => {
|
|
1762
|
+
expect(model.title).toBeUndefined();
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
test('should return meta title when defined', () => {
|
|
1766
|
+
TestFlowModel.define({
|
|
1767
|
+
label: 'Test Model Title',
|
|
1768
|
+
group: 'test',
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
const modelWithMeta = new TestFlowModel(modelOptions);
|
|
1772
|
+
expect(modelWithMeta.title).toBe('Test Model Title');
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
test('should translate meta title using translate method', () => {
|
|
1776
|
+
TestFlowModel.define({
|
|
1777
|
+
label: 'model.title.key',
|
|
1778
|
+
group: 'test',
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
const mockTranslate = vi.fn((v) => {
|
|
1782
|
+
if (v) return 'Translated Title';
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
const flowEngine = new FlowEngine();
|
|
1786
|
+
flowEngine.translate = mockTranslate;
|
|
1787
|
+
|
|
1788
|
+
const modelWithTranslate = new TestFlowModel({
|
|
1789
|
+
...modelOptions,
|
|
1790
|
+
flowEngine,
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
const title = modelWithTranslate.title;
|
|
1794
|
+
|
|
1795
|
+
expect(mockTranslate).toHaveBeenLastCalledWith('model.title.key');
|
|
1796
|
+
expect(title).toBe('Translated Title');
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
test('should return instance title when set via setTitle', () => {
|
|
1800
|
+
TestFlowModel.define({
|
|
1801
|
+
label: 'Meta Title',
|
|
1802
|
+
group: 'test',
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
const modelWithBoth = new TestFlowModel(modelOptions);
|
|
1806
|
+
modelWithBoth.setTitle('Instance Title');
|
|
1807
|
+
|
|
1808
|
+
expect(modelWithBoth.title).toBe('Instance Title');
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
test('should prioritize instance title over meta title', () => {
|
|
1812
|
+
TestFlowModel.define({
|
|
1813
|
+
label: 'Meta Title',
|
|
1814
|
+
group: 'test',
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
const modelWithBoth = new TestFlowModel(modelOptions);
|
|
1818
|
+
modelWithBoth.setTitle('Instance Title');
|
|
1819
|
+
|
|
1820
|
+
// Instance title should have higher priority
|
|
1821
|
+
expect(modelWithBoth.title).toBe('Instance Title');
|
|
1822
|
+
expect(modelWithBoth.title).not.toBe('Meta Title');
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
test('should fall back to meta title when instance title is cleared', () => {
|
|
1826
|
+
TestFlowModel.define({
|
|
1827
|
+
label: 'Meta Title',
|
|
1828
|
+
group: 'test',
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
const modelWithBoth = new TestFlowModel(modelOptions);
|
|
1832
|
+
modelWithBoth.setTitle('Instance Title');
|
|
1833
|
+
expect(modelWithBoth.title).toBe('Instance Title');
|
|
1834
|
+
|
|
1835
|
+
// Clear instance title
|
|
1836
|
+
modelWithBoth.setTitle('');
|
|
1837
|
+
expect(modelWithBoth.title).toBe('Meta Title');
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
test('should handle null and undefined instance titles', () => {
|
|
1841
|
+
TestFlowModel.define({
|
|
1842
|
+
label: 'Meta Title',
|
|
1843
|
+
group: 'test',
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
const modelWithMeta = new TestFlowModel(modelOptions);
|
|
1847
|
+
|
|
1848
|
+
// Test with null
|
|
1849
|
+
modelWithMeta.setTitle(null as any);
|
|
1850
|
+
expect(modelWithMeta.title).toBe('Meta Title');
|
|
1851
|
+
|
|
1852
|
+
// Test with undefined
|
|
1853
|
+
modelWithMeta.setTitle(undefined as any);
|
|
1854
|
+
expect(modelWithMeta.title).toBe('Meta Title');
|
|
1855
|
+
});
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
describe('setTitle method', () => {
|
|
1859
|
+
test('should set instance title correctly', () => {
|
|
1860
|
+
model.setTitle('Custom Title');
|
|
1861
|
+
expect(model.title).toBe('Custom Title');
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
test('should update title when called multiple times', () => {
|
|
1865
|
+
model.setTitle('First Title');
|
|
1866
|
+
expect(model.title).toBe('First Title');
|
|
1867
|
+
|
|
1868
|
+
model.setTitle('Second Title');
|
|
1869
|
+
expect(model.title).toBe('Second Title');
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
test('should handle empty string title', () => {
|
|
1873
|
+
TestFlowModel.define({
|
|
1874
|
+
label: 'Meta Title',
|
|
1875
|
+
group: 'test',
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
const modelWithMeta = new TestFlowModel(modelOptions);
|
|
1879
|
+
modelWithMeta.setTitle('Initial Title');
|
|
1880
|
+
expect(modelWithMeta.title).toBe('Initial Title');
|
|
1881
|
+
|
|
1882
|
+
// Empty string is falsy, so it falls back to meta title
|
|
1883
|
+
modelWithMeta.setTitle('');
|
|
1884
|
+
expect(modelWithMeta.title).toBe('Meta Title');
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
test('should handle special characters in title', () => {
|
|
1888
|
+
const specialTitle = 'Title with 特殊字符 & symbols!@#$%^&*()';
|
|
1889
|
+
model.setTitle(specialTitle);
|
|
1890
|
+
expect(model.title).toBe(specialTitle);
|
|
1891
|
+
});
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
describe('title inheritance', () => {
|
|
1895
|
+
test('should not inherit meta title from parent class by default', () => {
|
|
1896
|
+
const ParentModel = class extends FlowModel {};
|
|
1897
|
+
ParentModel.define({
|
|
1898
|
+
label: 'Parent Title',
|
|
1899
|
+
group: 'parent',
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
const ChildModel = class extends ParentModel {};
|
|
1903
|
+
|
|
1904
|
+
const parentInstance = new ParentModel(modelOptions);
|
|
1905
|
+
const childInstance = new ChildModel(modelOptions);
|
|
1906
|
+
|
|
1907
|
+
expect(parentInstance.title).toBe('Parent Title');
|
|
1908
|
+
// Child class doesn't inherit parent meta by default
|
|
1909
|
+
expect(childInstance.title).toBeUndefined();
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
test('should override parent meta title with child meta title', () => {
|
|
1913
|
+
const ParentModel = class extends FlowModel {};
|
|
1914
|
+
ParentModel.define({
|
|
1915
|
+
label: 'Parent Title',
|
|
1916
|
+
group: 'parent',
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
const ChildModel = class extends ParentModel {};
|
|
1920
|
+
ChildModel.define({
|
|
1921
|
+
label: 'Child Title',
|
|
1922
|
+
group: 'child',
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
const parentInstance = new ParentModel(modelOptions);
|
|
1926
|
+
const childInstance = new ChildModel(modelOptions);
|
|
1927
|
+
|
|
1928
|
+
expect(parentInstance.title).toBe('Parent Title');
|
|
1929
|
+
expect(childInstance.title).toBe('Child Title');
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
test('should allow instance title to override meta title', () => {
|
|
1933
|
+
const ParentModel = class extends FlowModel {};
|
|
1934
|
+
ParentModel.define({
|
|
1935
|
+
label: 'Parent Title',
|
|
1936
|
+
group: 'parent',
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
const ChildModel = class extends ParentModel {};
|
|
1940
|
+
ChildModel.define({
|
|
1941
|
+
label: 'Child Title',
|
|
1942
|
+
group: 'child',
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
const childInstance = new ChildModel(modelOptions);
|
|
1946
|
+
expect(childInstance.title).toBe('Child Title');
|
|
1947
|
+
|
|
1948
|
+
childInstance.setTitle('Instance Override');
|
|
1949
|
+
expect(childInstance.title).toBe('Instance Override');
|
|
1950
|
+
});
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
describe('title with translation', () => {
|
|
1954
|
+
test('should call translate method for meta title', () => {
|
|
1955
|
+
const mockTranslate = vi.fn((v) => {
|
|
1956
|
+
if (v) return 'Translated Meta Title';
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
TestFlowModel.define({
|
|
1960
|
+
label: 'meta.title.key',
|
|
1961
|
+
group: 'test',
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
const flowEngine = new FlowEngine();
|
|
1965
|
+
flowEngine.translate = mockTranslate;
|
|
1966
|
+
|
|
1967
|
+
const modelWithTranslate = new TestFlowModel({
|
|
1968
|
+
...modelOptions,
|
|
1969
|
+
flowEngine,
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
const title = modelWithTranslate.title;
|
|
1973
|
+
|
|
1974
|
+
expect(mockTranslate).toHaveBeenLastCalledWith('meta.title.key');
|
|
1975
|
+
expect(title).toBe('Translated Meta Title');
|
|
1976
|
+
});
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
describe('title serialization', () => {
|
|
1980
|
+
test('should not include instance title in serialization by default', () => {
|
|
1981
|
+
model.setTitle('Instance Title');
|
|
1982
|
+
const serialized = model.serialize();
|
|
1983
|
+
|
|
1984
|
+
// Instance title should not be included in serialization
|
|
1985
|
+
expect(serialized).not.toHaveProperty('title');
|
|
1986
|
+
expect(serialized).not.toHaveProperty('_title');
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
// this means after deserialization, should set title in some flow step
|
|
1990
|
+
test('should maintain title after serialization/deserialization cycle', () => {
|
|
1991
|
+
TestFlowModel.define({
|
|
1992
|
+
label: 'Meta Title',
|
|
1993
|
+
group: 'test',
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
const originalModel = new TestFlowModel(modelOptions);
|
|
1997
|
+
originalModel.setTitle('Instance Title');
|
|
1998
|
+
|
|
1999
|
+
const serialized = originalModel.serialize();
|
|
2000
|
+
const newModel = new TestFlowModel({
|
|
2001
|
+
...serialized,
|
|
2002
|
+
flowEngine,
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
// Meta title should be available
|
|
2006
|
+
expect(newModel.title).toBe('Meta Title');
|
|
2007
|
+
|
|
2008
|
+
// Instance title needs to be set again
|
|
2009
|
+
newModel.setTitle('Instance Title');
|
|
2010
|
+
expect(newModel.title).toBe('Instance Title');
|
|
2011
|
+
});
|
|
2012
|
+
});
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
// ==================== CACHE INVALIDATION ====================
|
|
2016
|
+
describe('Cache Invalidation', () => {
|
|
2017
|
+
let model: FlowModel;
|
|
2018
|
+
let realFlowEngine: FlowEngine;
|
|
2019
|
+
let deleteSpy: any;
|
|
2020
|
+
|
|
2021
|
+
beforeEach(() => {
|
|
2022
|
+
realFlowEngine = new FlowEngine();
|
|
2023
|
+
deleteSpy = vi.spyOn(realFlowEngine.applyFlowCache, 'delete');
|
|
2024
|
+
|
|
2025
|
+
// Mock generateApplyFlowCacheKey static method
|
|
2026
|
+
vi.spyOn(FlowEngine, 'generateApplyFlowCacheKey').mockImplementation(
|
|
2027
|
+
(prefix: string, type: string, modelUid: string) => `${prefix}-${type}-${modelUid}`,
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
model = new FlowModel({
|
|
2031
|
+
uid: 'test-model-uid',
|
|
2032
|
+
flowEngine: realFlowEngine,
|
|
2033
|
+
stepParams: {},
|
|
2034
|
+
subModels: {},
|
|
2035
|
+
});
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
afterEach(() => {
|
|
2039
|
+
vi.restoreAllMocks();
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
describe('invalidateAutoFlowCache', () => {
|
|
2043
|
+
test('should delete auto flow cache for current model', () => {
|
|
2044
|
+
const expectedCacheKey = 'autoFlow-all-test-model-uid';
|
|
2045
|
+
realFlowEngine.applyFlowCache.set(expectedCacheKey, {
|
|
2046
|
+
status: 'resolved',
|
|
2047
|
+
data: [],
|
|
2048
|
+
promise: Promise.resolve([]),
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
model.invalidateAutoFlowCache();
|
|
2052
|
+
|
|
2053
|
+
expect(deleteSpy).toHaveBeenCalledWith(expectedCacheKey);
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
test('should delete cache entries for all forks', () => {
|
|
2057
|
+
const fork1 = model.createFork();
|
|
2058
|
+
const fork2 = model.createFork();
|
|
2059
|
+
|
|
2060
|
+
const fork1CacheKey = `${fork1.forkId}-all-test-model-uid`;
|
|
2061
|
+
const fork2CacheKey = `${fork2.forkId}-all-test-model-uid`;
|
|
2062
|
+
|
|
2063
|
+
realFlowEngine.applyFlowCache.set(fork1CacheKey, {
|
|
2064
|
+
status: 'resolved',
|
|
2065
|
+
data: [],
|
|
2066
|
+
promise: Promise.resolve([]),
|
|
2067
|
+
});
|
|
2068
|
+
realFlowEngine.applyFlowCache.set(fork2CacheKey, {
|
|
2069
|
+
status: 'resolved',
|
|
2070
|
+
data: [],
|
|
2071
|
+
promise: Promise.resolve([]),
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
model.invalidateAutoFlowCache();
|
|
2075
|
+
|
|
2076
|
+
expect(deleteSpy).toHaveBeenCalledWith(fork1CacheKey);
|
|
2077
|
+
expect(deleteSpy).toHaveBeenCalledWith(fork2CacheKey);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
test('should recursively invalidate cache for array subModels', () => {
|
|
2081
|
+
const childModel1 = new FlowModel({ uid: 'child1', flowEngine: realFlowEngine });
|
|
2082
|
+
const childModel2 = new FlowModel({ uid: 'child2', flowEngine: realFlowEngine });
|
|
2083
|
+
|
|
2084
|
+
const child1Spy = vi.spyOn(childModel1, 'invalidateAutoFlowCache');
|
|
2085
|
+
const child2Spy = vi.spyOn(childModel2, 'invalidateAutoFlowCache');
|
|
2086
|
+
|
|
2087
|
+
model.addSubModel('children', childModel1);
|
|
2088
|
+
model.addSubModel('children', childModel2);
|
|
2089
|
+
|
|
2090
|
+
model.invalidateAutoFlowCache(true);
|
|
2091
|
+
|
|
2092
|
+
expect(child1Spy).toHaveBeenCalledWith(true);
|
|
2093
|
+
expect(child2Spy).toHaveBeenCalledWith(true);
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
test('should recursively invalidate cache for object subModels', () => {
|
|
2097
|
+
const childModel = new FlowModel({ uid: 'child', flowEngine: realFlowEngine });
|
|
2098
|
+
const childSpy = vi.spyOn(childModel, 'invalidateAutoFlowCache');
|
|
2099
|
+
|
|
2100
|
+
model.setSubModel('child', childModel);
|
|
2101
|
+
|
|
2102
|
+
model.invalidateAutoFlowCache(true);
|
|
2103
|
+
|
|
2104
|
+
expect(childSpy).toHaveBeenCalledWith(true);
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
test('should handle mixed array and object subModels', () => {
|
|
2108
|
+
const arrayChild1 = new FlowModel({ uid: 'arrayChild1', flowEngine: realFlowEngine });
|
|
2109
|
+
const arrayChild2 = new FlowModel({ uid: 'arrayChild2', flowEngine: realFlowEngine });
|
|
2110
|
+
const objectChild = new FlowModel({ uid: 'objectChild', flowEngine: realFlowEngine });
|
|
2111
|
+
|
|
2112
|
+
const array1Spy = vi.spyOn(arrayChild1, 'invalidateAutoFlowCache');
|
|
2113
|
+
const array2Spy = vi.spyOn(arrayChild2, 'invalidateAutoFlowCache');
|
|
2114
|
+
const objectSpy = vi.spyOn(objectChild, 'invalidateAutoFlowCache');
|
|
2115
|
+
|
|
2116
|
+
model.addSubModel('arrayChildren', arrayChild1);
|
|
2117
|
+
model.addSubModel('arrayChildren', arrayChild2);
|
|
2118
|
+
model.setSubModel('objectChild', objectChild);
|
|
2119
|
+
|
|
2120
|
+
model.invalidateAutoFlowCache(true);
|
|
2121
|
+
|
|
2122
|
+
expect(array1Spy).toHaveBeenCalledWith(true);
|
|
2123
|
+
expect(array2Spy).toHaveBeenCalledWith(true);
|
|
2124
|
+
expect(objectSpy).toHaveBeenCalledWith(true);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
test('should handle empty subModels without error', () => {
|
|
2128
|
+
model.invalidateAutoFlowCache();
|
|
2129
|
+
|
|
2130
|
+
expect(deleteSpy).toHaveBeenCalledWith('autoFlow-all-test-model-uid');
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
test('should handle null flowEngine gracefully', () => {
|
|
2134
|
+
const modelWithValidEngine = new FlowModel({ uid: 'test', flowEngine: realFlowEngine });
|
|
2135
|
+
modelWithValidEngine.flowEngine = null;
|
|
2136
|
+
|
|
2137
|
+
expect(() => {
|
|
2138
|
+
modelWithValidEngine.invalidateAutoFlowCache();
|
|
2139
|
+
}).not.toThrow();
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
test('should pass deep parameter to recursive calls', () => {
|
|
2143
|
+
const childModel = new FlowModel({ uid: 'child', flowEngine: realFlowEngine });
|
|
2144
|
+
const childSpy = vi.spyOn(childModel, 'invalidateAutoFlowCache');
|
|
2145
|
+
|
|
2146
|
+
model.setSubModel('child', childModel);
|
|
2147
|
+
|
|
2148
|
+
model.invalidateAutoFlowCache(true);
|
|
2149
|
+
|
|
2150
|
+
expect(childSpy).toHaveBeenCalledWith(true);
|
|
2151
|
+
});
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
// ==================== EXPRESSION RESOLUTION ====================
|
|
2156
|
+
describe('Expression Resolution', () => {
|
|
2157
|
+
let model: FlowModel;
|
|
2158
|
+
let TestFlowModel: typeof FlowModel;
|
|
2159
|
+
|
|
2160
|
+
beforeEach(() => {
|
|
2161
|
+
TestFlowModel = class extends FlowModel<any> {};
|
|
2162
|
+
model = new TestFlowModel({
|
|
2163
|
+
...modelOptions,
|
|
2164
|
+
uid: 'test-expression-model-uid',
|
|
2165
|
+
});
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
describe('{{ctx.xxx.yyy.zzz}} expression resolution', () => {
|
|
2169
|
+
test('should resolve simple ctx expressions in step parameters', async () => {
|
|
2170
|
+
const flow: FlowDefinitionOptions = {
|
|
2171
|
+
key: 'expressionFlow',
|
|
2172
|
+
|
|
2173
|
+
steps: {
|
|
2174
|
+
testStep: {
|
|
2175
|
+
handler: vi.fn().mockImplementation((ctx, params) => {
|
|
2176
|
+
// 验证表达式被正确解析
|
|
2177
|
+
expect(params.message).toBe('Hello Test User');
|
|
2178
|
+
expect(params.id).toBe('user123');
|
|
2179
|
+
return 'resolved';
|
|
2180
|
+
}),
|
|
2181
|
+
defaultParams: {
|
|
2182
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2183
|
+
id: '{{ctx.user.id}}',
|
|
2184
|
+
},
|
|
2185
|
+
},
|
|
2186
|
+
},
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
TestFlowModel.registerFlow(flow);
|
|
2190
|
+
|
|
2191
|
+
model.context.defineProperty('user', {
|
|
2192
|
+
get: () => {
|
|
2193
|
+
return {
|
|
2194
|
+
name: 'Test User',
|
|
2195
|
+
id: 'user123',
|
|
2196
|
+
};
|
|
2197
|
+
},
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
const handlerSpy = flow.steps.testStep.handler as any;
|
|
2201
|
+
await model.applyAutoFlows();
|
|
2202
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
test('should resolve nested ctx expressions with multiple levels', async () => {
|
|
2206
|
+
const flow: FlowDefinitionOptions = {
|
|
2207
|
+
key: 'nestedExpressionFlow',
|
|
2208
|
+
|
|
2209
|
+
steps: {
|
|
2210
|
+
nestedStep: {
|
|
2211
|
+
handler: vi.fn().mockImplementation((ctx, params) => {
|
|
2212
|
+
expect(params.authorName).toBe('John Doe');
|
|
2213
|
+
expect(params.authorEmail).toBe('john@example.com');
|
|
2214
|
+
expect(params.config).toBe('production');
|
|
2215
|
+
return 'nested-resolved';
|
|
2216
|
+
}),
|
|
2217
|
+
defaultParams: {
|
|
2218
|
+
authorName: '{{ctx.article.author.profile.name}}',
|
|
2219
|
+
authorEmail: '{{ctx.article.author.profile.email}}',
|
|
2220
|
+
config: '{{ctx.app.settings.environment}}',
|
|
2221
|
+
},
|
|
2222
|
+
},
|
|
2223
|
+
},
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
TestFlowModel.registerFlow(flow);
|
|
2227
|
+
|
|
2228
|
+
model.context.defineProperty('article', {
|
|
2229
|
+
get: () => ({
|
|
2230
|
+
author: {
|
|
2231
|
+
profile: {
|
|
2232
|
+
name: 'John Doe',
|
|
2233
|
+
email: 'john@example.com',
|
|
2234
|
+
},
|
|
2235
|
+
},
|
|
2236
|
+
}),
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
model.context.defineProperty('app', {
|
|
2240
|
+
get: () => ({
|
|
2241
|
+
settings: {
|
|
2242
|
+
environment: 'production',
|
|
2243
|
+
},
|
|
2244
|
+
}),
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
const handlerSpy = flow.steps.nestedStep.handler as any;
|
|
2248
|
+
await model.applyAutoFlows();
|
|
2249
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
2250
|
+
});
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2254
|
+
// ==================== USE RAW PARAMS ====================
|
|
2255
|
+
describe('useRawParams Functionality', () => {
|
|
2256
|
+
let model: FlowModel;
|
|
2257
|
+
let TestFlowModel: typeof FlowModel;
|
|
2258
|
+
|
|
2259
|
+
beforeEach(() => {
|
|
2260
|
+
TestFlowModel = class extends FlowModel<any> {};
|
|
2261
|
+
model = new TestFlowModel({
|
|
2262
|
+
...modelOptions,
|
|
2263
|
+
uid: 'test-raw-params-model-uid',
|
|
2264
|
+
});
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
describe('useRawParams: true (skip expression resolution)', () => {
|
|
2268
|
+
test('should skip expression resolution when useRawParams is true on action', async () => {
|
|
2269
|
+
const actionHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2270
|
+
// Parameters should contain raw expressions, not resolved values
|
|
2271
|
+
expect(params.message).toBe('Hello {{ctx.user.name}}');
|
|
2272
|
+
expect(params.id).toBe('{{ctx.user.id}}');
|
|
2273
|
+
return 'action-result';
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
2277
|
+
handler: actionHandler,
|
|
2278
|
+
useRawParams: true,
|
|
2279
|
+
defaultParams: {
|
|
2280
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2281
|
+
id: '{{ctx.user.id}}',
|
|
2282
|
+
},
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
model.flowEngine.registerActions({
|
|
2286
|
+
testAction: {
|
|
2287
|
+
name: 'testAction',
|
|
2288
|
+
handler: actionHandler,
|
|
2289
|
+
useRawParams: true,
|
|
2290
|
+
defaultParams: {
|
|
2291
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2292
|
+
id: '{{ctx.user.id}}',
|
|
2293
|
+
},
|
|
2294
|
+
},
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
const actionFlow: FlowDefinitionOptions = {
|
|
2298
|
+
key: 'rawParamsActionFlow',
|
|
2299
|
+
steps: {
|
|
2300
|
+
rawParamsStep: {
|
|
2301
|
+
use: 'testAction',
|
|
2302
|
+
},
|
|
2303
|
+
},
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
TestFlowModel.registerFlow(actionFlow);
|
|
2307
|
+
|
|
2308
|
+
model.context.defineProperty('user', {
|
|
2309
|
+
get: () => ({
|
|
2310
|
+
name: 'Test User',
|
|
2311
|
+
id: 'user123',
|
|
2312
|
+
}),
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
await model.applyFlow('rawParamsActionFlow');
|
|
2316
|
+
|
|
2317
|
+
expect(model.flowEngine.getAction).toHaveBeenCalledWith('testAction');
|
|
2318
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
2319
|
+
expect.any(Object),
|
|
2320
|
+
expect.objectContaining({
|
|
2321
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2322
|
+
id: '{{ctx.user.id}}',
|
|
2323
|
+
}),
|
|
2324
|
+
);
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2327
|
+
test('should skip expression resolution when useRawParams is true on step', async () => {
|
|
2328
|
+
const stepHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2329
|
+
// Parameters should contain raw expressions, not resolved values
|
|
2330
|
+
expect(params.title).toBe('Article: {{ctx.record.title}}');
|
|
2331
|
+
expect(params.userId).toBe('{{ctx.user.id}}');
|
|
2332
|
+
return 'step-result';
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
const stepFlow: FlowDefinitionOptions = {
|
|
2336
|
+
key: 'rawParamsStepFlow',
|
|
2337
|
+
steps: {
|
|
2338
|
+
rawParamsStep: {
|
|
2339
|
+
handler: stepHandler,
|
|
2340
|
+
useRawParams: true,
|
|
2341
|
+
defaultParams: {
|
|
2342
|
+
title: 'Article: {{ctx.record.title}}',
|
|
2343
|
+
userId: '{{ctx.user.id}}',
|
|
2344
|
+
},
|
|
2345
|
+
},
|
|
2346
|
+
},
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
TestFlowModel.registerFlow(stepFlow);
|
|
2350
|
+
|
|
2351
|
+
model.context.defineProperty('record', {
|
|
2352
|
+
get: () => ({ title: 'Test Article' }),
|
|
2353
|
+
});
|
|
2354
|
+
model.context.defineProperty('user', {
|
|
2355
|
+
get: () => ({ id: 123 }),
|
|
2356
|
+
});
|
|
2357
|
+
|
|
2358
|
+
await model.applyFlow('rawParamsStepFlow');
|
|
2359
|
+
|
|
2360
|
+
expect(stepHandler).toHaveBeenCalledWith(
|
|
2361
|
+
expect.any(Object),
|
|
2362
|
+
expect.objectContaining({
|
|
2363
|
+
title: 'Article: {{ctx.record.title}}',
|
|
2364
|
+
userId: '{{ctx.user.id}}',
|
|
2365
|
+
}),
|
|
2366
|
+
);
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
test('should prioritize step useRawParams over action useRawParams', async () => {
|
|
2370
|
+
const actionHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2371
|
+
// Step useRawParams: true should override action useRawParams: false
|
|
2372
|
+
expect(params.message).toBe('Hello {{ctx.user.name}}');
|
|
2373
|
+
return 'priority-result';
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
2377
|
+
handler: actionHandler,
|
|
2378
|
+
useRawParams: false, // Action says resolve expressions
|
|
2379
|
+
defaultParams: {
|
|
2380
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2381
|
+
},
|
|
2382
|
+
});
|
|
2383
|
+
|
|
2384
|
+
const priorityFlow: FlowDefinitionOptions = {
|
|
2385
|
+
key: 'priorityFlow',
|
|
2386
|
+
steps: {
|
|
2387
|
+
priorityStep: {
|
|
2388
|
+
use: 'testAction',
|
|
2389
|
+
useRawParams: true, // Step overrides action
|
|
2390
|
+
},
|
|
2391
|
+
},
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
TestFlowModel.registerFlow(priorityFlow);
|
|
2395
|
+
|
|
2396
|
+
model.context.defineProperty('user', {
|
|
2397
|
+
get: () => ({ name: 'Test User' }),
|
|
2398
|
+
});
|
|
2399
|
+
|
|
2400
|
+
await model.applyFlow('priorityFlow');
|
|
2401
|
+
|
|
2402
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
2403
|
+
expect.any(Object),
|
|
2404
|
+
expect.objectContaining({
|
|
2405
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2406
|
+
}),
|
|
2407
|
+
);
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
describe('useRawParams: false (normal expression resolution)', () => {
|
|
2412
|
+
test('should resolve expressions when useRawParams is false', async () => {
|
|
2413
|
+
const actionHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2414
|
+
// Parameters should be resolved
|
|
2415
|
+
expect(params.message).toBe('Hello Test User');
|
|
2416
|
+
expect(params.id).toBe('user123');
|
|
2417
|
+
return 'resolved-result';
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
2421
|
+
handler: actionHandler,
|
|
2422
|
+
useRawParams: false,
|
|
2423
|
+
defaultParams: {
|
|
2424
|
+
message: 'Hello {{ctx.user.name}}',
|
|
2425
|
+
id: '{{ctx.user.id}}',
|
|
2426
|
+
},
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
const resolvedFlow: FlowDefinitionOptions = {
|
|
2430
|
+
key: 'resolvedFlow',
|
|
2431
|
+
steps: {
|
|
2432
|
+
resolvedStep: {
|
|
2433
|
+
use: 'testAction',
|
|
2434
|
+
},
|
|
2435
|
+
},
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
TestFlowModel.registerFlow(resolvedFlow);
|
|
2439
|
+
|
|
2440
|
+
model.context.defineProperty('user', {
|
|
2441
|
+
get: () => ({
|
|
2442
|
+
name: 'Test User',
|
|
2443
|
+
id: 'user123',
|
|
2444
|
+
}),
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
await model.applyFlow('resolvedFlow');
|
|
2448
|
+
|
|
2449
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
2450
|
+
expect.any(Object),
|
|
2451
|
+
expect.objectContaining({
|
|
2452
|
+
message: 'Hello Test User',
|
|
2453
|
+
id: 'user123',
|
|
2454
|
+
}),
|
|
2455
|
+
);
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
describe('useRawParams as function', () => {
|
|
2460
|
+
test('should evaluate useRawParams function and skip resolution when returns true', async () => {
|
|
2461
|
+
const useRawParamsFunction = vi.fn().mockReturnValue(true);
|
|
2462
|
+
const actionHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2463
|
+
expect(params.message).toBe('Dynamic {{ctx.user.name}}');
|
|
2464
|
+
return 'dynamic-result';
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
2468
|
+
handler: actionHandler,
|
|
2469
|
+
useRawParams: useRawParamsFunction,
|
|
2470
|
+
defaultParams: {
|
|
2471
|
+
message: 'Dynamic {{ctx.user.name}}',
|
|
2472
|
+
},
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
const dynamicFlow: FlowDefinitionOptions = {
|
|
2476
|
+
key: 'dynamicFlow',
|
|
2477
|
+
steps: {
|
|
2478
|
+
dynamicStep: {
|
|
2479
|
+
use: 'testAction',
|
|
2480
|
+
},
|
|
2481
|
+
},
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2484
|
+
TestFlowModel.registerFlow(dynamicFlow);
|
|
2485
|
+
|
|
2486
|
+
model.context.defineProperty('user', {
|
|
2487
|
+
get: () => ({ name: 'Test User' }),
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
await model.applyFlow('dynamicFlow');
|
|
2491
|
+
|
|
2492
|
+
expect(useRawParamsFunction).toHaveBeenCalledWith(expect.any(Object));
|
|
2493
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
2494
|
+
expect.any(Object),
|
|
2495
|
+
expect.objectContaining({
|
|
2496
|
+
message: 'Dynamic {{ctx.user.name}}',
|
|
2497
|
+
}),
|
|
2498
|
+
);
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
test('should evaluate useRawParams function and resolve expressions when returns false', async () => {
|
|
2502
|
+
const useRawParamsFunction = vi.fn().mockReturnValue(false);
|
|
2503
|
+
const actionHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2504
|
+
expect(params.message).toBe('Dynamic Test User');
|
|
2505
|
+
return 'dynamic-resolved-result';
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
2509
|
+
handler: actionHandler,
|
|
2510
|
+
useRawParams: useRawParamsFunction,
|
|
2511
|
+
defaultParams: {
|
|
2512
|
+
message: 'Dynamic {{ctx.user.name}}',
|
|
2513
|
+
},
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
const dynamicResolvedFlow: FlowDefinitionOptions = {
|
|
2517
|
+
key: 'dynamicResolvedFlow',
|
|
2518
|
+
steps: {
|
|
2519
|
+
dynamicResolvedStep: {
|
|
2520
|
+
use: 'testAction',
|
|
2521
|
+
},
|
|
2522
|
+
},
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
TestFlowModel.registerFlow(dynamicResolvedFlow);
|
|
2526
|
+
|
|
2527
|
+
model.context.defineProperty('user', {
|
|
2528
|
+
get: () => ({ name: 'Test User' }),
|
|
2529
|
+
});
|
|
2530
|
+
|
|
2531
|
+
await model.applyFlow('dynamicResolvedFlow');
|
|
2532
|
+
|
|
2533
|
+
expect(useRawParamsFunction).toHaveBeenCalledWith(expect.any(Object));
|
|
2534
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
2535
|
+
expect.any(Object),
|
|
2536
|
+
expect.objectContaining({
|
|
2537
|
+
message: 'Dynamic Test User',
|
|
2538
|
+
}),
|
|
2539
|
+
);
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
test('should handle async useRawParams function', async () => {
|
|
2543
|
+
const asyncUseRawParamsFunction = vi.fn().mockResolvedValue(true);
|
|
2544
|
+
const actionHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2545
|
+
expect(params.message).toBe('Async {{ctx.user.name}}');
|
|
2546
|
+
return 'async-result';
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
model.flowEngine.getAction = vi.fn().mockReturnValue({
|
|
2550
|
+
handler: actionHandler,
|
|
2551
|
+
useRawParams: asyncUseRawParamsFunction,
|
|
2552
|
+
defaultParams: {
|
|
2553
|
+
message: 'Async {{ctx.user.name}}',
|
|
2554
|
+
},
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
const asyncFlow: FlowDefinitionOptions = {
|
|
2558
|
+
key: 'asyncFlow',
|
|
2559
|
+
steps: {
|
|
2560
|
+
asyncStep: {
|
|
2561
|
+
use: 'testAction',
|
|
2562
|
+
},
|
|
2563
|
+
},
|
|
2564
|
+
};
|
|
2565
|
+
|
|
2566
|
+
TestFlowModel.registerFlow(asyncFlow);
|
|
2567
|
+
|
|
2568
|
+
model.context.defineProperty('user', {
|
|
2569
|
+
get: () => ({ name: 'Test User' }),
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
await model.applyFlow('asyncFlow');
|
|
2573
|
+
|
|
2574
|
+
expect(asyncUseRawParamsFunction).toHaveBeenCalledWith(expect.any(Object));
|
|
2575
|
+
expect(actionHandler).toHaveBeenCalledWith(
|
|
2576
|
+
expect.any(Object),
|
|
2577
|
+
expect.objectContaining({
|
|
2578
|
+
message: 'Async {{ctx.user.name}}',
|
|
2579
|
+
}),
|
|
2580
|
+
);
|
|
2581
|
+
});
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
describe('complex useRawParams scenarios', () => {
|
|
2585
|
+
test('should handle mixed steps with different useRawParams settings', async () => {
|
|
2586
|
+
const stepHandler1 = vi.fn().mockImplementation((ctx, params) => {
|
|
2587
|
+
// This step should have raw expressions
|
|
2588
|
+
expect(params.message).toBe('Raw: {{ctx.user.name}}');
|
|
2589
|
+
return 'raw-result';
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
const stepHandler2 = vi.fn().mockImplementation((ctx, params) => {
|
|
2593
|
+
// This step should have resolved expressions
|
|
2594
|
+
expect(params.message).toBe('Resolved: Test User');
|
|
2595
|
+
return 'resolved-result';
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
const mixedFlow: FlowDefinitionOptions = {
|
|
2599
|
+
key: 'mixedFlow',
|
|
2600
|
+
steps: {
|
|
2601
|
+
rawStep: {
|
|
2602
|
+
handler: stepHandler1,
|
|
2603
|
+
useRawParams: true,
|
|
2604
|
+
defaultParams: {
|
|
2605
|
+
message: 'Raw: {{ctx.user.name}}',
|
|
2606
|
+
},
|
|
2607
|
+
},
|
|
2608
|
+
resolvedStep: {
|
|
2609
|
+
handler: stepHandler2,
|
|
2610
|
+
useRawParams: false,
|
|
2611
|
+
defaultParams: {
|
|
2612
|
+
message: 'Resolved: {{ctx.user.name}}',
|
|
2613
|
+
},
|
|
2614
|
+
},
|
|
2615
|
+
},
|
|
2616
|
+
};
|
|
2617
|
+
|
|
2618
|
+
TestFlowModel.registerFlow(mixedFlow);
|
|
2619
|
+
|
|
2620
|
+
model.context.defineProperty('user', {
|
|
2621
|
+
get: () => ({ name: 'Test User' }),
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
const result = await model.applyFlow('mixedFlow');
|
|
2625
|
+
|
|
2626
|
+
expect(result).toEqual({
|
|
2627
|
+
rawStep: 'raw-result',
|
|
2628
|
+
resolvedStep: 'resolved-result',
|
|
2629
|
+
});
|
|
2630
|
+
expect(stepHandler1).toHaveBeenCalledWith(
|
|
2631
|
+
expect.any(Object),
|
|
2632
|
+
expect.objectContaining({
|
|
2633
|
+
message: 'Raw: {{ctx.user.name}}',
|
|
2634
|
+
}),
|
|
2635
|
+
);
|
|
2636
|
+
expect(stepHandler2).toHaveBeenCalledWith(
|
|
2637
|
+
expect.any(Object),
|
|
2638
|
+
expect.objectContaining({
|
|
2639
|
+
message: 'Resolved: Test User',
|
|
2640
|
+
}),
|
|
2641
|
+
);
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
test('should work with model step parameters', async () => {
|
|
2645
|
+
const stepHandler = vi.fn().mockImplementation((ctx, params) => {
|
|
2646
|
+
// Should get raw expressions since useRawParams is true
|
|
2647
|
+
expect(params.defaultMessage).toBe('Default: {{ctx.user.name}}');
|
|
2648
|
+
expect(params.modelMessage).toBe('Model: {{ctx.user.email}}');
|
|
2649
|
+
return 'model-params-result';
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
const modelParamsFlow: FlowDefinitionOptions = {
|
|
2653
|
+
key: 'modelParamsFlow',
|
|
2654
|
+
steps: {
|
|
2655
|
+
modelStep: {
|
|
2656
|
+
handler: stepHandler,
|
|
2657
|
+
useRawParams: true,
|
|
2658
|
+
defaultParams: {
|
|
2659
|
+
defaultMessage: 'Default: {{ctx.user.name}}',
|
|
2660
|
+
},
|
|
2661
|
+
},
|
|
2662
|
+
},
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
TestFlowModel.registerFlow(modelParamsFlow);
|
|
2666
|
+
|
|
2667
|
+
// Set model step parameters
|
|
2668
|
+
model.setStepParams({
|
|
2669
|
+
modelParamsFlow: {
|
|
2670
|
+
modelStep: {
|
|
2671
|
+
modelMessage: 'Model: {{ctx.user.email}}',
|
|
2672
|
+
},
|
|
2673
|
+
},
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
model.context.defineProperty('user', {
|
|
2677
|
+
get: () => ({
|
|
2678
|
+
name: 'Test User',
|
|
2679
|
+
email: 'test@example.com',
|
|
2680
|
+
}),
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
await model.applyFlow('modelParamsFlow');
|
|
2684
|
+
|
|
2685
|
+
expect(stepHandler).toHaveBeenCalledWith(
|
|
2686
|
+
expect.any(Object),
|
|
2687
|
+
expect.objectContaining({
|
|
2688
|
+
defaultMessage: 'Default: {{ctx.user.name}}',
|
|
2689
|
+
modelMessage: 'Model: {{ctx.user.email}}',
|
|
2690
|
+
}),
|
|
2691
|
+
);
|
|
2692
|
+
});
|
|
2693
|
+
});
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// ==================== EDGE CASES ====================
|
|
2697
|
+
describe('Edge Cases & Error Handling', () => {
|
|
2698
|
+
test('should handle model destruction gracefully', () => {
|
|
2699
|
+
const model = new FlowModel(modelOptions);
|
|
2700
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
2701
|
+
|
|
2702
|
+
model.createFork();
|
|
2703
|
+
model.setProps({ testProp: 'value' });
|
|
2704
|
+
|
|
2705
|
+
try {
|
|
2706
|
+
expect(() => model.remove()).not.toThrow();
|
|
2707
|
+
} finally {
|
|
2708
|
+
consoleSpy.mockRestore();
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
test('should handle flows with no steps', async () => {
|
|
2713
|
+
const emptyFlow: FlowDefinitionOptions = {
|
|
2714
|
+
key: 'emptyFlow',
|
|
2715
|
+
steps: {},
|
|
2716
|
+
};
|
|
2717
|
+
|
|
2718
|
+
const TestModel = class extends FlowModel {};
|
|
2719
|
+
TestModel.registerFlow(emptyFlow);
|
|
2720
|
+
|
|
2721
|
+
const model = new TestModel(modelOptions);
|
|
2722
|
+
const result = await model.applyFlow('emptyFlow');
|
|
2723
|
+
|
|
2724
|
+
expect(result).toEqual({});
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
test('should handle concurrent flow executions', async () => {
|
|
2728
|
+
const flow = createBasicFlowDefinition();
|
|
2729
|
+
const TestModel = class extends FlowModel {};
|
|
2730
|
+
TestModel.registerFlow(flow);
|
|
2731
|
+
|
|
2732
|
+
const model = new TestModel(modelOptions);
|
|
2733
|
+
|
|
2734
|
+
const promises = Array.from({ length: 3 }, () => model.applyFlow(flow.key));
|
|
2735
|
+
|
|
2736
|
+
const results = await Promise.all(promises);
|
|
2737
|
+
|
|
2738
|
+
results.forEach((result) => {
|
|
2739
|
+
expect(result).toEqual({
|
|
2740
|
+
step1: 'step1-result',
|
|
2741
|
+
step2: 'step2-result',
|
|
2742
|
+
});
|
|
2743
|
+
});
|
|
2744
|
+
});
|
|
2745
|
+
});
|
|
2746
|
+
});
|