@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.
Files changed (597) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +30 -0
  3. package/lib/ContextPathProxy.d.ts +17 -0
  4. package/lib/ContextPathProxy.js +65 -0
  5. package/lib/ElementProxy.d.ts +17 -0
  6. package/lib/ElementProxy.js +93 -0
  7. package/lib/FlowContextProvider.d.ts +24 -0
  8. package/lib/FlowContextProvider.js +82 -0
  9. package/lib/FlowDefinition.d.ts +423 -0
  10. package/lib/FlowDefinition.js +257 -0
  11. package/lib/JSRunner.d.ts +32 -0
  12. package/lib/JSRunner.js +95 -0
  13. package/lib/ReactView.d.ts +20 -0
  14. package/lib/ReactView.js +120 -0
  15. package/lib/ViewScopedFlowEngine.d.ts +23 -0
  16. package/lib/ViewScopedFlowEngine.js +81 -0
  17. package/lib/acl/Acl.d.ts +31 -0
  18. package/lib/acl/Acl.js +115 -0
  19. package/lib/action-registry/BaseActionRegistry.d.ts +23 -0
  20. package/lib/action-registry/BaseActionRegistry.js +57 -0
  21. package/lib/action-registry/EngineActionRegistry.d.ts +20 -0
  22. package/lib/action-registry/EngineActionRegistry.js +47 -0
  23. package/lib/action-registry/ModelActionRegistry.d.ts +34 -0
  24. package/lib/action-registry/ModelActionRegistry.js +79 -0
  25. package/lib/components/DynamicFlowsEditor.d.ts +17 -0
  26. package/lib/components/DynamicFlowsEditor.js +49 -0
  27. package/lib/components/FieldModelRenderer.d.ts +10 -0
  28. package/lib/components/FieldModelRenderer.js +94 -0
  29. package/lib/components/FlowContextSelector.d.ts +11 -0
  30. package/lib/components/FlowContextSelector.js +221 -0
  31. package/lib/components/FlowErrorFallback.d.ts +25 -0
  32. package/lib/components/FlowErrorFallback.js +264 -0
  33. package/lib/components/FlowModelRenderer.d.ts +64 -0
  34. package/lib/components/FlowModelRenderer.js +254 -0
  35. package/lib/components/FormItem.d.ts +18 -0
  36. package/lib/components/FormItem.js +147 -0
  37. package/lib/components/common/FlowSettingsButton.d.ts +11 -0
  38. package/lib/components/common/FlowSettingsButton.js +66 -0
  39. package/lib/components/common/index.d.ts +9 -0
  40. package/lib/components/common/index.js +30 -0
  41. package/lib/components/common/withFlowDesignMode.d.ts +26 -0
  42. package/lib/components/common/withFlowDesignMode.js +61 -0
  43. package/lib/components/dnd/getMousePositionOnElement.d.ts +50 -0
  44. package/lib/components/dnd/getMousePositionOnElement.js +95 -0
  45. package/lib/components/dnd/index.d.ts +24 -0
  46. package/lib/components/dnd/index.js +164 -0
  47. package/lib/components/dnd/moveBlock.d.ts +33 -0
  48. package/lib/components/dnd/moveBlock.js +302 -0
  49. package/lib/components/index.d.ts +18 -0
  50. package/lib/components/index.js +48 -0
  51. package/lib/components/settings/independents/dropdown/FlowsDropdownButton.d.ts +46 -0
  52. package/lib/components/settings/independents/dropdown/FlowsDropdownButton.js +225 -0
  53. package/lib/components/settings/independents/dropdown/index.d.ts +9 -0
  54. package/lib/components/settings/independents/dropdown/index.js +30 -0
  55. package/lib/components/settings/independents/index.d.ts +1 -0
  56. package/lib/components/settings/independents/index.js +30 -0
  57. package/lib/components/settings/index.d.ts +10 -0
  58. package/lib/components/settings/index.js +32 -0
  59. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +24 -0
  60. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +501 -0
  61. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.d.ts +45 -0
  62. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +231 -0
  63. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +111 -0
  64. package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +484 -0
  65. package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.d.ts +26 -0
  66. package/lib/components/settings/wrappers/contextual/StepRequiredSettingsDialog.js +342 -0
  67. package/lib/components/settings/wrappers/contextual/StepSettings.d.ts +23 -0
  68. package/lib/components/settings/wrappers/contextual/StepSettings.js +110 -0
  69. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.d.ts +20 -0
  70. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +206 -0
  71. package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.d.ts +20 -0
  72. package/lib/components/settings/wrappers/contextual/StepSettingsDrawer.js +47 -0
  73. package/lib/components/settings/wrappers/contextual/index.d.ts +14 -0
  74. package/lib/components/settings/wrappers/contextual/index.js +40 -0
  75. package/lib/components/settings/wrappers/embedded/FlowSettings.d.ts +33 -0
  76. package/lib/components/settings/wrappers/embedded/FlowSettings.js +207 -0
  77. package/lib/components/settings/wrappers/embedded/FlowsSettings.d.ts +41 -0
  78. package/lib/components/settings/wrappers/embedded/FlowsSettings.js +84 -0
  79. package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.d.ts +16 -0
  80. package/lib/components/settings/wrappers/embedded/FlowsSettingsContent.js +88 -0
  81. package/lib/components/settings/wrappers/embedded/index.d.ts +10 -0
  82. package/lib/components/settings/wrappers/embedded/index.js +32 -0
  83. package/lib/components/settings/wrappers/index.d.ts +2 -0
  84. package/lib/components/settings/wrappers/index.js +32 -0
  85. package/lib/components/subModel/AddSubModelButton.d.ts +62 -0
  86. package/lib/components/subModel/AddSubModelButton.js +415 -0
  87. package/lib/components/subModel/LazyDropdown.d.ts +55 -0
  88. package/lib/components/subModel/LazyDropdown.js +524 -0
  89. package/lib/components/subModel/index.d.ts +10 -0
  90. package/lib/components/subModel/index.js +32 -0
  91. package/lib/components/subModel/utils.d.ts +34 -0
  92. package/lib/components/subModel/utils.js +287 -0
  93. package/lib/components/variables/InlineVariableTag.d.ts +21 -0
  94. package/lib/components/variables/InlineVariableTag.js +123 -0
  95. package/lib/components/variables/SlateVariableEditor.d.ts +46 -0
  96. package/lib/components/variables/SlateVariableEditor.js +302 -0
  97. package/lib/components/variables/VariableInput.d.ts +11 -0
  98. package/lib/components/variables/VariableInput.js +322 -0
  99. package/lib/components/variables/VariableTag.d.ts +11 -0
  100. package/lib/components/variables/VariableTag.js +148 -0
  101. package/lib/components/variables/VariableTrigger.d.ts +20 -0
  102. package/lib/components/variables/VariableTrigger.js +136 -0
  103. package/lib/components/variables/index.d.ts +15 -0
  104. package/lib/components/variables/index.js +53 -0
  105. package/lib/components/variables/types.d.ts +93 -0
  106. package/lib/components/variables/types.js +24 -0
  107. package/lib/components/variables/useResolvedMetaTree.d.ts +19 -0
  108. package/lib/components/variables/useResolvedMetaTree.js +91 -0
  109. package/lib/components/variables/utils.d.ts +22 -0
  110. package/lib/components/variables/utils.js +177 -0
  111. package/lib/data-source/index.d.ts +180 -0
  112. package/lib/data-source/index.js +733 -0
  113. package/lib/data-source/jioToJoiSchema.d.ts +19 -0
  114. package/lib/data-source/jioToJoiSchema.js +114 -0
  115. package/lib/decorators/index.d.ts +9 -0
  116. package/lib/decorators/index.js +36 -0
  117. package/lib/decorators/largeField.d.ts +9 -0
  118. package/lib/decorators/largeField.js +42 -0
  119. package/lib/emitter.d.ts +16 -0
  120. package/lib/emitter.js +58 -0
  121. package/lib/event-registry/BaseEventRegistry.d.ts +22 -0
  122. package/lib/event-registry/BaseEventRegistry.js +57 -0
  123. package/lib/event-registry/EngineEventRegistry.d.ts +19 -0
  124. package/lib/event-registry/EngineEventRegistry.js +47 -0
  125. package/lib/event-registry/ModelEventRegistry.d.ts +33 -0
  126. package/lib/event-registry/ModelEventRegistry.js +79 -0
  127. package/lib/executor/FlowExecutor.d.ts +26 -0
  128. package/lib/executor/FlowExecutor.js +262 -0
  129. package/lib/flow-registry/BaseFlowRegistry.d.ts +46 -0
  130. package/lib/flow-registry/BaseFlowRegistry.js +86 -0
  131. package/lib/flow-registry/GlobalFlowRegistry.d.ts +22 -0
  132. package/lib/flow-registry/GlobalFlowRegistry.js +95 -0
  133. package/lib/flow-registry/InstanceFlowRegistry.d.ts +21 -0
  134. package/lib/flow-registry/InstanceFlowRegistry.js +59 -0
  135. package/lib/flow-registry/index.d.ts +11 -0
  136. package/lib/flow-registry/index.js +34 -0
  137. package/lib/flowContext.d.ts +215 -0
  138. package/lib/flowContext.js +1266 -0
  139. package/lib/flowEngine.d.ts +340 -0
  140. package/lib/flowEngine.js +781 -0
  141. package/lib/flowI18n.d.ts +46 -0
  142. package/lib/flowI18n.js +117 -0
  143. package/lib/flowSettings.d.ts +266 -0
  144. package/lib/flowSettings.js +850 -0
  145. package/lib/hooks/index.d.ts +14 -0
  146. package/lib/hooks/index.js +40 -0
  147. package/lib/hooks/useApplyAutoFlows.d.ts +21 -0
  148. package/lib/hooks/useApplyAutoFlows.js +62 -0
  149. package/lib/hooks/useFlowModel.d.ts +29 -0
  150. package/lib/hooks/useFlowModel.js +72 -0
  151. package/lib/hooks/useFlowModelById.d.ts +11 -0
  152. package/lib/hooks/useFlowModelById.js +61 -0
  153. package/lib/hooks/useFlowSettingsContext.d.ts +20 -0
  154. package/lib/hooks/useFlowSettingsContext.js +61 -0
  155. package/lib/hooks/useFlowStep.d.ts +17 -0
  156. package/lib/hooks/useFlowStep.js +56 -0
  157. package/lib/hooks/useNiceDropdownMaxHeight.d.ts +13 -0
  158. package/lib/hooks/useNiceDropdownMaxHeight.js +52 -0
  159. package/lib/index.d.ts +27 -0
  160. package/lib/index.js +73 -0
  161. package/lib/locale/en-US.json +61 -0
  162. package/lib/locale/index.d.ts +141 -0
  163. package/lib/locale/index.js +70 -0
  164. package/lib/locale/zh-CN.json +61 -0
  165. package/lib/models/CollectionFieldModel.d.ts +50 -0
  166. package/lib/models/CollectionFieldModel.js +242 -0
  167. package/lib/models/DisplayItemModel.d.ts +12 -0
  168. package/lib/models/DisplayItemModel.js +41 -0
  169. package/lib/models/EditableItemModel.d.ts +12 -0
  170. package/lib/models/EditableItemModel.js +41 -0
  171. package/lib/models/FilterableItemModel.d.ts +12 -0
  172. package/lib/models/FilterableItemModel.js +41 -0
  173. package/lib/models/flowModel.d.ts +344 -0
  174. package/lib/models/flowModel.js +1133 -0
  175. package/lib/models/forkFlowModel.d.ts +83 -0
  176. package/lib/models/forkFlowModel.js +257 -0
  177. package/lib/models/index.d.ts +14 -0
  178. package/lib/models/index.js +40 -0
  179. package/lib/provider.d.ts +22 -0
  180. package/lib/provider.js +114 -0
  181. package/lib/resources/apiResource.d.ts +34 -0
  182. package/lib/resources/apiResource.js +153 -0
  183. package/lib/resources/baseRecordResource.d.ts +61 -0
  184. package/lib/resources/baseRecordResource.js +264 -0
  185. package/lib/resources/filterItem.d.ts +33 -0
  186. package/lib/resources/filterItem.js +93 -0
  187. package/lib/resources/flowResource.d.ts +45 -0
  188. package/lib/resources/flowResource.js +146 -0
  189. package/lib/resources/index.d.ts +15 -0
  190. package/lib/resources/index.js +42 -0
  191. package/lib/resources/multiRecordResource.d.ts +53 -0
  192. package/lib/resources/multiRecordResource.js +230 -0
  193. package/lib/resources/singleRecordResource.d.ts +23 -0
  194. package/lib/resources/singleRecordResource.js +111 -0
  195. package/lib/resources/sqlResource.d.ts +73 -0
  196. package/lib/resources/sqlResource.js +294 -0
  197. package/lib/runjs-context/contexts/FlowRunJSContext.d.ts +38 -0
  198. package/lib/runjs-context/contexts/FlowRunJSContext.js +217 -0
  199. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.d.ts +16 -0
  200. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +66 -0
  201. package/lib/runjs-context/contexts/JSBlockRunJSContext.d.ts +16 -0
  202. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +78 -0
  203. package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.d.ts +12 -0
  204. package/lib/runjs-context/contexts/JSCollectionActionRunJSContext.js +59 -0
  205. package/lib/runjs-context/contexts/JSFieldRunJSContext.d.ts +16 -0
  206. package/lib/runjs-context/contexts/JSFieldRunJSContext.js +70 -0
  207. package/lib/runjs-context/contexts/JSItemRunJSContext.d.ts +16 -0
  208. package/lib/runjs-context/contexts/JSItemRunJSContext.js +66 -0
  209. package/lib/runjs-context/contexts/JSRecordActionRunJSContext.d.ts +12 -0
  210. package/lib/runjs-context/contexts/JSRecordActionRunJSContext.js +61 -0
  211. package/lib/runjs-context/contexts/LinkageRunJSContext.d.ts +12 -0
  212. package/lib/runjs-context/contexts/LinkageRunJSContext.js +62 -0
  213. package/lib/runjs-context/helpers.d.ts +17 -0
  214. package/lib/runjs-context/helpers.js +79 -0
  215. package/lib/runjs-context/index.d.ts +19 -0
  216. package/lib/runjs-context/index.js +57 -0
  217. package/lib/runjs-context/registry.d.ts +17 -0
  218. package/lib/runjs-context/registry.js +93 -0
  219. package/lib/runjs-context/snippets/global/api-request-get.snippet.d.ts +16 -0
  220. package/lib/runjs-context/snippets/global/api-request-get.snippet.js +42 -0
  221. package/lib/runjs-context/snippets/global/api-request-post.snippet.d.ts +16 -0
  222. package/lib/runjs-context/snippets/global/api-request-post.snippet.js +42 -0
  223. package/lib/runjs-context/snippets/global/console-log-ctx.snippet.d.ts +16 -0
  224. package/lib/runjs-context/snippets/global/console-log-ctx.snippet.js +41 -0
  225. package/lib/runjs-context/snippets/global/copy-record-json.snippet.d.ts +11 -0
  226. package/lib/runjs-context/snippets/global/copy-record-json.snippet.js +42 -0
  227. package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.d.ts +11 -0
  228. package/lib/runjs-context/snippets/global/copy-to-clipboard.snippet.js +42 -0
  229. package/lib/runjs-context/snippets/global/log-json-record.snippet.d.ts +11 -0
  230. package/lib/runjs-context/snippets/global/log-json-record.snippet.js +42 -0
  231. package/lib/runjs-context/snippets/global/message-error.snippet.d.ts +11 -0
  232. package/lib/runjs-context/snippets/global/message-error.snippet.js +41 -0
  233. package/lib/runjs-context/snippets/global/message-success.snippet.d.ts +11 -0
  234. package/lib/runjs-context/snippets/global/message-success.snippet.js +41 -0
  235. package/lib/runjs-context/snippets/global/notification-open.snippet.d.ts +16 -0
  236. package/lib/runjs-context/snippets/global/notification-open.snippet.js +43 -0
  237. package/lib/runjs-context/snippets/global/open-view-dialog.snippet.d.ts +11 -0
  238. package/lib/runjs-context/snippets/global/open-view-dialog.snippet.js +47 -0
  239. package/lib/runjs-context/snippets/global/open-view-drawer.snippet.d.ts +11 -0
  240. package/lib/runjs-context/snippets/global/open-view-drawer.snippet.js +47 -0
  241. package/lib/runjs-context/snippets/global/requireAsync.snippet.d.ts +16 -0
  242. package/lib/runjs-context/snippets/global/requireAsync.snippet.js +46 -0
  243. package/lib/runjs-context/snippets/global/sleep.snippet.d.ts +16 -0
  244. package/lib/runjs-context/snippets/global/sleep.snippet.js +43 -0
  245. package/lib/runjs-context/snippets/global/try-catch-async.snippet.d.ts +16 -0
  246. package/lib/runjs-context/snippets/global/try-catch-async.snippet.js +44 -0
  247. package/lib/runjs-context/snippets/global/view-navigation-push.snippet.d.ts +11 -0
  248. package/lib/runjs-context/snippets/global/view-navigation-push.snippet.js +44 -0
  249. package/lib/runjs-context/snippets/global/window-open.snippet.d.ts +16 -0
  250. package/lib/runjs-context/snippets/global/window-open.snippet.js +41 -0
  251. package/lib/runjs-context/snippets/index.d.ts +11 -0
  252. package/lib/runjs-context/snippets/index.js +94 -0
  253. package/lib/runjs-context/snippets/libs/echarts-init.snippet.d.ts +15 -0
  254. package/lib/runjs-context/snippets/libs/echarts-init.snippet.js +46 -0
  255. package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.d.ts +15 -0
  256. package/lib/runjs-context/snippets/scene/actions/collection-selected-count.snippet.js +44 -0
  257. package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.d.ts +15 -0
  258. package/lib/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.js +43 -0
  259. package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.d.ts +15 -0
  260. package/lib/runjs-context/snippets/scene/actions/record-id-message.snippet.js +43 -0
  261. package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.d.ts +15 -0
  262. package/lib/runjs-context/snippets/scene/actions/run-action-basic.snippet.js +40 -0
  263. package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.d.ts +15 -0
  264. package/lib/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.js +46 -0
  265. package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.d.ts +15 -0
  266. package/lib/runjs-context/snippets/scene/jsblock/append-style.snippet.js +42 -0
  267. package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.d.ts +15 -0
  268. package/lib/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.js +46 -0
  269. package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.d.ts +15 -0
  270. package/lib/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.js +41 -0
  271. package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.d.ts +15 -0
  272. package/lib/runjs-context/snippets/scene/jsblock/render-basic.snippet.js +41 -0
  273. package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.d.ts +15 -0
  274. package/lib/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.js +46 -0
  275. package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.d.ts +11 -0
  276. package/lib/runjs-context/snippets/scene/jsblock/render-card.snippet.js +45 -0
  277. package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.d.ts +15 -0
  278. package/lib/runjs-context/snippets/scene/jsblock/render-react.snippet.js +56 -0
  279. package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.d.ts +15 -0
  280. package/lib/runjs-context/snippets/scene/jsfield/color-by-value.snippet.js +42 -0
  281. package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.d.ts +15 -0
  282. package/lib/runjs-context/snippets/scene/jsfield/format-number.snippet.js +41 -0
  283. package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.d.ts +15 -0
  284. package/lib/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.js +40 -0
  285. package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.d.ts +15 -0
  286. package/lib/runjs-context/snippets/scene/jsitem/render-basic.snippet.js +44 -0
  287. package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.d.ts +15 -0
  288. package/lib/runjs-context/snippets/scene/linkage/set-disabled.snippet.js +60 -0
  289. package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.d.ts +15 -0
  290. package/lib/runjs-context/snippets/scene/linkage/set-field-value.snippet.js +59 -0
  291. package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.d.ts +15 -0
  292. package/lib/runjs-context/snippets/scene/linkage/set-required.snippet.js +60 -0
  293. package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.d.ts +15 -0
  294. package/lib/runjs-context/snippets/scene/linkage/toggle-visible.snippet.js +60 -0
  295. package/lib/runjs-context/snippets/types.d.ts +16 -0
  296. package/lib/runjs-context/snippets/types.js +24 -0
  297. package/lib/types.d.ts +398 -0
  298. package/lib/types.js +43 -0
  299. package/lib/utils/autoFlowError.d.ts +16 -0
  300. package/lib/utils/autoFlowError.js +53 -0
  301. package/lib/utils/constants.d.ts +29 -0
  302. package/lib/utils/constants.js +77 -0
  303. package/lib/utils/context.d.ts +40 -0
  304. package/lib/utils/context.js +63 -0
  305. package/lib/utils/createCollectionContextMeta.d.ts +11 -0
  306. package/lib/utils/createCollectionContextMeta.js +117 -0
  307. package/lib/utils/exceptions.d.ts +22 -0
  308. package/lib/utils/exceptions.js +62 -0
  309. package/lib/utils/flow-definitions.d.ts +11 -0
  310. package/lib/utils/flow-definitions.js +40 -0
  311. package/lib/utils/index.d.ts +22 -0
  312. package/lib/utils/index.js +117 -0
  313. package/lib/utils/inheritance.d.ts +16 -0
  314. package/lib/utils/inheritance.js +53 -0
  315. package/lib/utils/params-resolvers.d.ts +51 -0
  316. package/lib/utils/params-resolvers.js +309 -0
  317. package/lib/utils/parsePathnameToViewParams.d.ts +34 -0
  318. package/lib/utils/parsePathnameToViewParams.js +84 -0
  319. package/lib/utils/safeGlobals.d.ts +16 -0
  320. package/lib/utils/safeGlobals.js +179 -0
  321. package/lib/utils/schema-utils.d.ts +40 -0
  322. package/lib/utils/schema-utils.js +161 -0
  323. package/lib/utils/serverContextParams.d.ts +28 -0
  324. package/lib/utils/serverContextParams.js +106 -0
  325. package/lib/utils/setupRuntimeContextSteps.d.ts +19 -0
  326. package/lib/utils/setupRuntimeContextSteps.js +88 -0
  327. package/lib/utils/translation.d.ts +18 -0
  328. package/lib/utils/translation.js +58 -0
  329. package/lib/utils/variablesParams.d.ts +51 -0
  330. package/lib/utils/variablesParams.js +150 -0
  331. package/lib/views/DialogComponent.d.ts +22 -0
  332. package/lib/views/DialogComponent.js +98 -0
  333. package/lib/views/DrawerComponent.d.ts +11 -0
  334. package/lib/views/DrawerComponent.js +101 -0
  335. package/lib/views/FlowView.d.ts +76 -0
  336. package/lib/views/FlowView.js +81 -0
  337. package/lib/views/PageComponent.d.ts +10 -0
  338. package/lib/views/PageComponent.js +167 -0
  339. package/lib/views/ViewNavigation.d.ts +45 -0
  340. package/lib/views/ViewNavigation.js +97 -0
  341. package/lib/views/createViewMeta.d.ts +16 -0
  342. package/lib/views/createViewMeta.js +171 -0
  343. package/lib/views/index.d.ts +13 -0
  344. package/lib/views/index.js +48 -0
  345. package/lib/views/useDialog.d.ts +32 -0
  346. package/lib/views/useDialog.js +199 -0
  347. package/lib/views/useDrawer.d.ts +33 -0
  348. package/lib/views/useDrawer.js +206 -0
  349. package/lib/views/usePage.d.ts +32 -0
  350. package/lib/views/usePage.js +193 -0
  351. package/lib/views/usePatchElement.d.ts +10 -0
  352. package/lib/views/usePatchElement.js +54 -0
  353. package/lib/views/usePopover.d.ts +17 -0
  354. package/lib/views/usePopover.js +159 -0
  355. package/package.json +37 -0
  356. package/src/ContextPathProxy.ts +45 -0
  357. package/src/ElementProxy.ts +69 -0
  358. package/src/FlowContextProvider.tsx +40 -0
  359. package/src/FlowDefinition.ts +275 -0
  360. package/src/JSRunner.ts +84 -0
  361. package/src/ReactView.tsx +104 -0
  362. package/src/ViewScopedFlowEngine.ts +75 -0
  363. package/src/__tests__/ElementProxy.test.ts +51 -0
  364. package/src/__tests__/JSRunner.test.ts +92 -0
  365. package/src/__tests__/ReactView.test.tsx +63 -0
  366. package/src/__tests__/context-path-proxy.test.ts +35 -0
  367. package/src/__tests__/flow-engine.test.ts +189 -0
  368. package/src/__tests__/flowContext.test.ts +2012 -0
  369. package/src/__tests__/flowEngine.saveModel.test.ts +171 -0
  370. package/src/__tests__/flowI18n.test.ts +28 -0
  371. package/src/__tests__/flowModel.getFlows.test.ts +61 -0
  372. package/src/__tests__/flowModel.openView.navigation.test.ts +78 -0
  373. package/src/__tests__/flowRuntimeContext.test.ts +187 -0
  374. package/src/__tests__/flowSettings.open.test.tsx +1920 -0
  375. package/src/__tests__/flowSettings.test.ts +566 -0
  376. package/src/__tests__/globalFlowRegistry.test.ts +77 -0
  377. package/src/__tests__/isFieldInterfaceMatch.test.ts +51 -0
  378. package/src/__tests__/metaTreeNodeCache.test.ts +234 -0
  379. package/src/__tests__/path-aggregation.test.ts +85 -0
  380. package/src/__tests__/provider.test.tsx +28 -0
  381. package/src/__tests__/renderHiddenInConfig.test.tsx +91 -0
  382. package/src/__tests__/runjsContext.test.ts +60 -0
  383. package/src/__tests__/viewScopedFlowEngine.test.ts +212 -0
  384. package/src/acl/Acl.tsx +109 -0
  385. package/src/acl/__tests__/Acl.test.tsx +72 -0
  386. package/src/action-registry/BaseActionRegistry.ts +46 -0
  387. package/src/action-registry/EngineActionRegistry.ts +32 -0
  388. package/src/action-registry/ModelActionRegistry.ts +75 -0
  389. package/src/action-registry/__tests__/engineActionRegistry.test.ts +43 -0
  390. package/src/action-registry/__tests__/modelActionRegistry.test.ts +107 -0
  391. package/src/components/DynamicFlowsEditor.tsx +318 -0
  392. package/src/components/FieldModelRenderer.tsx +62 -0
  393. package/src/components/FlowContextSelector.tsx +255 -0
  394. package/src/components/FlowErrorFallback.tsx +316 -0
  395. package/src/components/FlowModelRenderer.tsx +428 -0
  396. package/src/components/FormItem.tsx +130 -0
  397. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +226 -0
  398. package/src/components/common/FlowSettingsButton.tsx +30 -0
  399. package/src/components/common/index.ts +10 -0
  400. package/src/components/common/withFlowDesignMode.tsx +49 -0
  401. package/src/components/dnd/getMousePositionOnElement.ts +115 -0
  402. package/src/components/dnd/index.tsx +128 -0
  403. package/src/components/dnd/moveBlock.ts +379 -0
  404. package/src/components/index.ts +20 -0
  405. package/src/components/settings/independents/dropdown/FlowsDropdownButton.tsx +279 -0
  406. package/src/components/settings/independents/dropdown/index.ts +10 -0
  407. package/src/components/settings/independents/index.ts +2 -0
  408. package/src/components/settings/index.ts +11 -0
  409. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +617 -0
  410. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +292 -0
  411. package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +655 -0
  412. package/src/components/settings/wrappers/contextual/StepRequiredSettingsDialog.tsx +446 -0
  413. package/src/components/settings/wrappers/contextual/StepSettings.tsx +109 -0
  414. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +217 -0
  415. package/src/components/settings/wrappers/contextual/StepSettingsDrawer.tsx +32 -0
  416. package/src/components/settings/wrappers/contextual/index.ts +15 -0
  417. package/src/components/settings/wrappers/embedded/FlowSettings.tsx +258 -0
  418. package/src/components/settings/wrappers/embedded/FlowsSettings.tsx +111 -0
  419. package/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx +96 -0
  420. package/src/components/settings/wrappers/embedded/index.ts +11 -0
  421. package/src/components/settings/wrappers/index.ts +5 -0
  422. package/src/components/subModel/AddSubModelButton.tsx +575 -0
  423. package/src/components/subModel/LazyDropdown.tsx +714 -0
  424. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +1185 -0
  425. package/src/components/subModel/__tests__/buildWrapperFieldChildren.test.ts +192 -0
  426. package/src/components/subModel/__tests__/utils.test.ts +425 -0
  427. package/src/components/subModel/index.ts +12 -0
  428. package/src/components/subModel/utils.ts +278 -0
  429. package/src/components/variables/InlineVariableTag.tsx +97 -0
  430. package/src/components/variables/SlateVariableEditor.tsx +384 -0
  431. package/src/components/variables/VariableInput.tsx +342 -0
  432. package/src/components/variables/VariableTag.tsx +123 -0
  433. package/src/components/variables/VariableTrigger.tsx +116 -0
  434. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +553 -0
  435. package/src/components/variables/__tests__/VariableInput.test.tsx +550 -0
  436. package/src/components/variables/__tests__/VariableTag.test.tsx +347 -0
  437. package/src/components/variables/__tests__/test-utils.tsx +62 -0
  438. package/src/components/variables/__tests__/utils.test.ts +310 -0
  439. package/src/components/variables/index.ts +16 -0
  440. package/src/components/variables/types.ts +100 -0
  441. package/src/components/variables/useResolvedMetaTree.ts +76 -0
  442. package/src/components/variables/utils.ts +192 -0
  443. package/src/data-source/__tests__/collection.test.ts +58 -0
  444. package/src/data-source/__tests__/index.test.ts +82 -0
  445. package/src/data-source/__tests__/jioToJoiSchema.test.ts +56 -0
  446. package/src/data-source/index.ts +812 -0
  447. package/src/data-source/jioToJoiSchema.ts +103 -0
  448. package/src/decorators/index.ts +10 -0
  449. package/src/decorators/largeField.ts +14 -0
  450. package/src/emitter.ts +33 -0
  451. package/src/event-registry/BaseEventRegistry.ts +40 -0
  452. package/src/event-registry/EngineEventRegistry.ts +26 -0
  453. package/src/event-registry/ModelEventRegistry.ts +69 -0
  454. package/src/event-registry/__tests__/engineEventRegistry.test.ts +48 -0
  455. package/src/executor/FlowExecutor.ts +256 -0
  456. package/src/executor/__tests__/eventStep.test.ts +157 -0
  457. package/src/executor/__tests__/flowExecutor.test.ts +163 -0
  458. package/src/flow-registry/BaseFlowRegistry.ts +91 -0
  459. package/src/flow-registry/GlobalFlowRegistry.ts +82 -0
  460. package/src/flow-registry/InstanceFlowRegistry.ts +39 -0
  461. package/src/flow-registry/__tests__/globalFlowRegistry.test.ts +141 -0
  462. package/src/flow-registry/__tests__/instance-and-global-registry.test.ts +67 -0
  463. package/src/flow-registry/__tests__/instanceFlowRegistry.test.ts +83 -0
  464. package/src/flow-registry/index.ts +12 -0
  465. package/src/flowContext.ts +1639 -0
  466. package/src/flowEngine.ts +905 -0
  467. package/src/flowI18n.ts +96 -0
  468. package/src/flowSettings.ts +1045 -0
  469. package/src/hooks/index.ts +15 -0
  470. package/src/hooks/useApplyAutoFlows.ts +51 -0
  471. package/src/hooks/useFlowModel.tsx +59 -0
  472. package/src/hooks/useFlowModelById.ts +37 -0
  473. package/src/hooks/useFlowSettingsContext.tsx +37 -0
  474. package/src/hooks/useFlowStep.tsx +19 -0
  475. package/src/hooks/useNiceDropdownMaxHeight.ts +34 -0
  476. package/src/index.ts +38 -0
  477. package/src/locale/en-US.json +61 -0
  478. package/src/locale/index.ts +38 -0
  479. package/src/locale/zh-CN.json +61 -0
  480. package/src/models/CollectionFieldModel.tsx +269 -0
  481. package/src/models/DisplayItemModel.tsx +13 -0
  482. package/src/models/EditableItemModel.tsx +13 -0
  483. package/src/models/FilterableItemModel.tsx +13 -0
  484. package/src/models/__tests__/CollectionFieldModel.test.ts +122 -0
  485. package/src/models/__tests__/defaultParams-on-create.test.ts +83 -0
  486. package/src/models/__tests__/flow-model-oninit.test.ts +44 -0
  487. package/src/models/__tests__/flowModel.actions.integration.test.ts +100 -0
  488. package/src/models/__tests__/flowModel.getFlows.sort.test.ts +100 -0
  489. package/src/models/__tests__/flowModel.test.ts +2746 -0
  490. package/src/models/__tests__/flowRegistry.test.ts +512 -0
  491. package/src/models/__tests__/forkFlowModel.test.ts +1047 -0
  492. package/src/models/__tests__/model-actions.test.ts +70 -0
  493. package/src/models/__tests__/model-events.test.ts +69 -0
  494. package/src/models/flowModel.tsx +1398 -0
  495. package/src/models/forkFlowModel.ts +287 -0
  496. package/src/models/index.ts +17 -0
  497. package/src/provider.tsx +101 -0
  498. package/src/resources/__tests__/apiResource.test.ts +201 -0
  499. package/src/resources/__tests__/baseRecordResource.test.ts +262 -0
  500. package/src/resources/__tests__/filterItem.test.ts +260 -0
  501. package/src/resources/__tests__/flowResource.test.ts +127 -0
  502. package/src/resources/apiResource.ts +148 -0
  503. package/src/resources/baseRecordResource.ts +279 -0
  504. package/src/resources/filterItem.ts +74 -0
  505. package/src/resources/flowResource.ts +143 -0
  506. package/src/resources/index.ts +17 -0
  507. package/src/resources/multiRecordResource.ts +219 -0
  508. package/src/resources/singleRecordResource.ts +83 -0
  509. package/src/resources/sqlResource.ts +299 -0
  510. package/src/runjs-context/contexts/FlowRunJSContext.ts +190 -0
  511. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +39 -0
  512. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +52 -0
  513. package/src/runjs-context/contexts/JSCollectionActionRunJSContext.ts +32 -0
  514. package/src/runjs-context/contexts/JSFieldRunJSContext.ts +43 -0
  515. package/src/runjs-context/contexts/JSItemRunJSContext.ts +39 -0
  516. package/src/runjs-context/contexts/JSRecordActionRunJSContext.ts +34 -0
  517. package/src/runjs-context/contexts/LinkageRunJSContext.ts +35 -0
  518. package/src/runjs-context/helpers.ts +56 -0
  519. package/src/runjs-context/index.ts +20 -0
  520. package/src/runjs-context/registry.ts +65 -0
  521. package/src/runjs-context/snippets/global/api-request-get.snippet.ts +20 -0
  522. package/src/runjs-context/snippets/global/api-request-post.snippet.ts +20 -0
  523. package/src/runjs-context/snippets/global/console-log-ctx.snippet.ts +19 -0
  524. package/src/runjs-context/snippets/global/copy-record-json.snippet.ts +21 -0
  525. package/src/runjs-context/snippets/global/copy-to-clipboard.snippet.ts +21 -0
  526. package/src/runjs-context/snippets/global/log-json-record.snippet.ts +21 -0
  527. package/src/runjs-context/snippets/global/message-error.snippet.ts +20 -0
  528. package/src/runjs-context/snippets/global/message-success.snippet.ts +20 -0
  529. package/src/runjs-context/snippets/global/notification-open.snippet.ts +21 -0
  530. package/src/runjs-context/snippets/global/open-view-dialog.snippet.ts +26 -0
  531. package/src/runjs-context/snippets/global/open-view-drawer.snippet.ts +26 -0
  532. package/src/runjs-context/snippets/global/requireAsync.snippet.ts +24 -0
  533. package/src/runjs-context/snippets/global/sleep.snippet.ts +21 -0
  534. package/src/runjs-context/snippets/global/try-catch-async.snippet.ts +22 -0
  535. package/src/runjs-context/snippets/global/view-navigation-push.snippet.ts +23 -0
  536. package/src/runjs-context/snippets/global/window-open.snippet.ts +19 -0
  537. package/src/runjs-context/snippets/index.ts +59 -0
  538. package/src/runjs-context/snippets/libs/echarts-init.snippet.ts +24 -0
  539. package/src/runjs-context/snippets/scene/actions/collection-selected-count.snippet.ts +22 -0
  540. package/src/runjs-context/snippets/scene/actions/iterate-selected-rows.snippet.ts +21 -0
  541. package/src/runjs-context/snippets/scene/actions/record-id-message.snippet.ts +21 -0
  542. package/src/runjs-context/snippets/scene/actions/run-action-basic.snippet.ts +18 -0
  543. package/src/runjs-context/snippets/scene/jsblock/add-event-listener.snippet.ts +29 -0
  544. package/src/runjs-context/snippets/scene/jsblock/append-style.snippet.ts +20 -0
  545. package/src/runjs-context/snippets/scene/jsblock/jsx-mount.snippet.ts +24 -0
  546. package/src/runjs-context/snippets/scene/jsblock/jsx-unmount.snippet.ts +19 -0
  547. package/src/runjs-context/snippets/scene/jsblock/render-basic.snippet.ts +24 -0
  548. package/src/runjs-context/snippets/scene/jsblock/render-button-handler.snippet.ts +24 -0
  549. package/src/runjs-context/snippets/scene/jsblock/render-card.snippet.ts +30 -0
  550. package/src/runjs-context/snippets/scene/jsblock/render-react.snippet.ts +34 -0
  551. package/src/runjs-context/snippets/scene/jsfield/color-by-value.snippet.ts +20 -0
  552. package/src/runjs-context/snippets/scene/jsfield/format-number.snippet.ts +19 -0
  553. package/src/runjs-context/snippets/scene/jsfield/innerHTML-value.snippet.ts +18 -0
  554. package/src/runjs-context/snippets/scene/jsitem/render-basic.snippet.ts +27 -0
  555. package/src/runjs-context/snippets/scene/linkage/set-disabled.snippet.ts +38 -0
  556. package/src/runjs-context/snippets/scene/linkage/set-field-value.snippet.ts +37 -0
  557. package/src/runjs-context/snippets/scene/linkage/set-required.snippet.ts +38 -0
  558. package/src/runjs-context/snippets/scene/linkage/toggle-visible.snippet.ts +38 -0
  559. package/src/runjs-context/snippets/types.ts +17 -0
  560. package/src/types.ts +474 -0
  561. package/src/utils/__tests__/context.test.ts +93 -0
  562. package/src/utils/__tests__/params-resolvers.test.ts +652 -0
  563. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +104 -0
  564. package/src/utils/__tests__/safeGlobals.test.ts +29 -0
  565. package/src/utils/__tests__/utils.test.ts +1021 -0
  566. package/src/utils/__tests__/variablesParams.test.ts +52 -0
  567. package/src/utils/autoFlowError.ts +29 -0
  568. package/src/utils/constants.ts +60 -0
  569. package/src/utils/context.ts +70 -0
  570. package/src/utils/createCollectionContextMeta.ts +122 -0
  571. package/src/utils/exceptions.ts +36 -0
  572. package/src/utils/flow-definitions.ts +16 -0
  573. package/src/utils/index.ts +63 -0
  574. package/src/utils/inheritance.ts +39 -0
  575. package/src/utils/params-resolvers.ts +482 -0
  576. package/src/utils/parsePathnameToViewParams.ts +103 -0
  577. package/src/utils/safeGlobals.ts +188 -0
  578. package/src/utils/schema-utils.ts +201 -0
  579. package/src/utils/serverContextParams.ts +111 -0
  580. package/src/utils/setupRuntimeContextSteps.ts +89 -0
  581. package/src/utils/translation.ts +37 -0
  582. package/src/utils/variablesParams.ts +175 -0
  583. package/src/views/DialogComponent.tsx +79 -0
  584. package/src/views/DrawerComponent.tsx +72 -0
  585. package/src/views/FlowView.tsx +103 -0
  586. package/src/views/PageComponent.tsx +150 -0
  587. package/src/views/ViewNavigation.ts +122 -0
  588. package/src/views/__tests__/FlowView.test.ts +31 -0
  589. package/src/views/__tests__/ViewNavigation.test.ts +191 -0
  590. package/src/views/__tests__/usePatchElement.test.tsx +28 -0
  591. package/src/views/createViewMeta.ts +157 -0
  592. package/src/views/index.tsx +14 -0
  593. package/src/views/useDialog.tsx +192 -0
  594. package/src/views/useDrawer.tsx +205 -0
  595. package/src/views/usePage.tsx +182 -0
  596. package/src/views/usePatchElement.tsx +27 -0
  597. 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
+ });