@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.11

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 (334) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/PluginSettingsManager.d.ts +33 -0
  4. package/es/RouteRepository.d.ts +29 -6
  5. package/es/RouterManager.d.ts +15 -0
  6. package/es/authRedirect.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  8. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  9. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  10. package/es/components/category-tabs/index.d.ts +9 -0
  11. package/es/components/form/JsonTextArea.d.ts +2 -1
  12. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  13. package/es/components/form/TypedVariableInput.d.ts +31 -5
  14. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  15. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  16. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  17. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  18. package/es/components/form/filter/index.d.ts +2 -0
  19. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  20. package/es/components/form/index.d.ts +1 -0
  21. package/es/components/index.d.ts +2 -0
  22. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  23. package/es/entry-actions/index.d.ts +9 -0
  24. package/es/flow/actions/afterSuccess.d.ts +8 -1
  25. package/es/flow/actions/index.d.ts +2 -2
  26. package/es/flow/actions/linkageRules.d.ts +2 -0
  27. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  28. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  29. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  30. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  31. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +3 -0
  32. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  33. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  34. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  35. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  36. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  37. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  38. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  39. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  40. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  41. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  42. package/es/flow/components/FlowRoute.d.ts +2 -2
  43. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  44. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  45. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  46. package/es/flow/index.d.ts +1 -0
  47. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  48. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  49. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  50. package/es/flow/models/base/GridModel.d.ts +1 -1
  51. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  52. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  53. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  54. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  55. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  56. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  57. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  58. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  59. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  60. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  61. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  62. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  63. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  64. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  65. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  66. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  67. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  68. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  69. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  70. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  71. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  72. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  73. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  74. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  75. package/es/flow/utils/formValueDeps.d.ts +32 -0
  76. package/es/flow-compat/FieldValidation.d.ts +2 -1
  77. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  78. package/es/flow-compat/routeTypes.d.ts +1 -0
  79. package/es/index.d.ts +8 -1
  80. package/es/index.mjs +367 -181
  81. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  82. package/es/utils/index.d.ts +1 -0
  83. package/es/utils/markdownSanitize.d.ts +11 -0
  84. package/lib/index.js +367 -181
  85. package/lib/locale/languageCodes.js +3 -1
  86. package/package.json +9 -7
  87. package/src/APIClient.ts +1 -10
  88. package/src/Application.tsx +4 -0
  89. package/src/BaseApplication.tsx +13 -6
  90. package/src/PluginSettingsManager.ts +53 -0
  91. package/src/RouteRepository.ts +151 -24
  92. package/src/RouterManager.tsx +142 -1
  93. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  94. package/src/__tests__/RouteRepository.test.ts +239 -0
  95. package/src/__tests__/RouterManager.test.ts +125 -0
  96. package/src/__tests__/app.test.tsx +73 -0
  97. package/src/__tests__/authRedirect.test.ts +17 -0
  98. package/src/__tests__/browserChecker.test.ts +61 -0
  99. package/src/__tests__/exports.test.ts +16 -0
  100. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  101. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +234 -2
  102. package/src/__tests__/plugin-manager.test.tsx +44 -2
  103. package/src/__tests__/settings-center.test.tsx +40 -1
  104. package/src/authRedirect.ts +38 -0
  105. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  106. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  107. package/src/collection-manager/field-configure.ts +1 -1
  108. package/src/collection-manager/field-validation.ts +1 -1
  109. package/src/collection-manager/interfaces/id.ts +1 -1
  110. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  111. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  112. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  113. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  114. package/src/collection-manager/interfaces/obo.tsx +2 -2
  115. package/src/collection-manager/interfaces/oho.tsx +2 -2
  116. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  117. package/src/collection-manager/interfaces/uuid.ts +1 -1
  118. package/src/components/AppComponents.tsx +19 -3
  119. package/src/components/README.md +7 -1
  120. package/src/components/README.zh-CN.md +6 -1
  121. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  122. package/src/components/category-tabs/index.ts +10 -0
  123. package/src/components/form/JsonTextArea.tsx +25 -11
  124. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  125. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  126. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  127. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  128. package/src/components/form/TypedVariableInput.tsx +452 -101
  129. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  130. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  131. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  132. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  133. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  134. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  135. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  136. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  137. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  138. package/src/components/form/filter/index.ts +2 -0
  139. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  140. package/src/components/form/index.tsx +1 -0
  141. package/src/components/index.ts +2 -0
  142. package/src/entry-actions/EntryActionManager.ts +76 -0
  143. package/src/entry-actions/index.ts +10 -0
  144. package/src/flow/FlowPage.tsx +38 -3
  145. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  146. package/src/flow/__tests__/FlowRoute.test.tsx +1181 -80
  147. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  148. package/src/flow/__tests__/getKey.test.ts +7 -0
  149. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  150. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +115 -0
  151. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  152. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  153. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1124 -0
  154. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  155. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  156. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  157. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  158. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  159. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  160. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  161. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  162. package/src/flow/actions/afterSuccess.tsx +142 -3
  163. package/src/flow/actions/customVariable.tsx +1 -2
  164. package/src/flow/actions/dataScopeFilter.ts +20 -1
  165. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  166. package/src/flow/actions/index.ts +3 -1
  167. package/src/flow/actions/linkageRules.tsx +209 -47
  168. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  169. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  170. package/src/flow/actions/openView.tsx +59 -5
  171. package/src/flow/actions/runjs.tsx +2 -14
  172. package/src/flow/actions/validation.tsx +62 -30
  173. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  174. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  175. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  176. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -14
  177. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  178. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  179. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  180. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  181. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  182. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  183. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  184. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  185. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  186. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  187. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +435 -0
  188. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  189. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  190. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  191. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  192. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  193. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  194. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  195. package/src/flow/common/Markdown/Display.tsx +14 -3
  196. package/src/flow/common/Markdown/Edit.tsx +19 -7
  197. package/src/flow/common/Markdown/style.ts +4 -0
  198. package/src/flow/components/BlockItemCard.tsx +2 -2
  199. package/src/flow/components/DefaultValue.tsx +1 -2
  200. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  201. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  202. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  203. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  204. package/src/flow/components/FlowRoute.tsx +256 -53
  205. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  206. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  207. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  208. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  209. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  210. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  211. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  212. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  213. package/src/flow/components/fieldAssignOptions.ts +155 -27
  214. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  215. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  216. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  217. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  218. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  219. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  220. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  221. package/src/flow/index.ts +4 -1
  222. package/src/flow/internal/components/Markdown/util.ts +2 -1
  223. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  224. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  225. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  226. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  227. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  228. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  229. package/src/flow/models/base/ActionModel.tsx +21 -8
  230. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  231. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  232. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  233. package/src/flow/models/base/GridModel.tsx +38 -7
  234. package/src/flow/models/base/PageModel/PageModel.tsx +387 -50
  235. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  236. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  237. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +817 -13
  238. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  239. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  240. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  241. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  242. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  243. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  244. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  245. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  246. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  247. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +59 -6
  248. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  249. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  250. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  251. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +143 -4
  252. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  253. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  254. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  255. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  256. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  257. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  258. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  259. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  260. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  261. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  262. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  263. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  264. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  265. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  266. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  267. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  268. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  269. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  270. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
  271. package/src/flow/models/blocks/table/TableBlockModel.tsx +42 -19
  272. package/src/flow/models/blocks/table/TableColumnModel.tsx +72 -6
  273. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  274. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  275. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  276. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  277. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  278. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +243 -1
  279. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  280. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  281. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  282. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  283. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +31 -7
  284. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  285. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  286. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  287. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  288. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  289. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  290. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  291. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  292. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  293. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  294. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  295. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  296. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  297. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  298. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  299. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  300. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  301. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  302. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  303. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  304. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  305. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  306. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  307. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  308. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  309. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  310. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  311. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  312. package/src/flow/routeTransientInputArgs.ts +27 -0
  313. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  314. package/src/flow/utils/dataScopeFormValueClear.ts +219 -81
  315. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  316. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  317. package/src/flow/utils/formValueDeps.ts +170 -0
  318. package/src/flow-compat/FieldValidation.tsx +122 -60
  319. package/src/flow-compat/Popover.tsx +43 -4
  320. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  321. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  322. package/src/flow-compat/routeTypes.ts +1 -0
  323. package/src/index.ts +14 -1
  324. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  325. package/src/locale/languageCodes.ts +3 -1
  326. package/src/nocobase-buildin-plugin/index.tsx +63 -15
  327. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  328. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  329. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  330. package/src/settings-center/plugin-manager/index.tsx +3 -0
  331. package/src/settings-center/utils.tsx +0 -6
  332. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  333. package/src/utils/index.tsx +1 -0
  334. package/src/utils/markdownSanitize.ts +88 -0
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { beforeEach, describe, expect, it, vi } from 'vitest';
11
11
  import type { DragEndEvent } from '@dnd-kit/core';
12
+ import { render, waitFor } from '@testing-library/react';
12
13
 
13
14
  // Mock FlowModel and other dependencies
14
15
  vi.mock('@nocobase/flow-engine', () => {
@@ -140,13 +141,9 @@ vi.mock('@nocobase/flow-engine', () => {
140
141
  jioToJoiSchema: () => ({
141
142
  validate: (value: any) => ({ value }),
142
143
  }),
143
- createSafeWindow: () => window,
144
- createSafeDocument: () => document,
145
- createSafeNavigator: () => navigator,
146
144
  isRunJSValue: () => false,
147
145
  isVariableExpression: () => false,
148
146
  normalizeRunJSValue: (value: any) => value,
149
- runjsWithSafeGlobals: async (handler: any, ...args: any[]) => handler?.(...args),
150
147
  parseCtxDateExpression: (value: any) => value,
151
148
  setupRunJSContexts: () => undefined,
152
149
  getRunJSScenesForContext: () => [],
@@ -225,6 +222,17 @@ describe('PageModel', () => {
225
222
  let pageModel: InstanceType<typeof PageModelClass>;
226
223
  let mockDragEndEvent: DragEndEvent;
227
224
 
225
+ const getDndChildren = (result: { props: { children?: unknown } }) => {
226
+ const children = result.props.children;
227
+ return Array.isArray(children) ? children : [children].filter(Boolean);
228
+ };
229
+
230
+ const getTabsElement = (result: { props: { children?: unknown } }) => getDndChildren(result)[1] as any;
231
+
232
+ const getTabSyncElement = (result: { props: { children?: unknown } }) => getDndChildren(result)[0] as any;
233
+
234
+ const getHiddenActiveTabContent = (result: { props: { children?: unknown } }) => getDndChildren(result)[2];
235
+
228
236
  beforeEach(async () => {
229
237
  vi.resetModules();
230
238
  vi.clearAllMocks();
@@ -303,9 +311,82 @@ describe('PageModel', () => {
303
311
  expect(blockOnActive).toHaveBeenCalledWith(true);
304
312
  expect(blockOnInactive).toHaveBeenCalledWith(false);
305
313
  });
314
+
315
+ it('should use the first actual tab for lifecycle when tabs are disabled even if it is hidden', () => {
316
+ const hiddenFirstTab = {
317
+ uid: 'tab-hidden-first',
318
+ hidden: true,
319
+ context: {},
320
+ subModels: { grid: { mapSubModels: vi.fn() } },
321
+ };
322
+ const visibleSecondTab = {
323
+ uid: 'tab-visible-second',
324
+ context: {},
325
+ subModels: { grid: { mapSubModels: vi.fn() } },
326
+ };
327
+ (pageModel as any).subModels = { tabs: [hiddenFirstTab, visibleSecondTab] };
328
+ pageModel.props = { enableTabs: false, tabActiveKey: 'tab-visible-second' } as any;
329
+ (pageModel as any).context = { view: { navigation: null } };
330
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
331
+
332
+ pageModel.activateCurrentTab();
333
+ pageModel.deactivateCurrentTab();
334
+
335
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden-first', 'onActive', false);
336
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-hidden-first', 'onInactive');
337
+ });
338
+
339
+ it('should keep a hidden active tab responsible for lifecycle when tabs are enabled', () => {
340
+ const hiddenActiveTab = {
341
+ uid: 'tab-hidden-active',
342
+ hidden: true,
343
+ context: {},
344
+ subModels: { grid: { mapSubModels: vi.fn() } },
345
+ };
346
+ const visibleTab = {
347
+ uid: 'tab-visible',
348
+ context: {},
349
+ subModels: { grid: { mapSubModels: vi.fn() } },
350
+ };
351
+ (pageModel as any).subModels = { tabs: [hiddenActiveTab, visibleTab] };
352
+ pageModel.props = { enableTabs: true, tabActiveKey: 'tab-hidden-active' } as any;
353
+ (pageModel as any).context = {
354
+ view: { navigation: { viewParams: { tabUid: 'tab-hidden-active' } } },
355
+ };
356
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
357
+
358
+ pageModel.activateCurrentTab();
359
+ pageModel.deactivateCurrentTab();
360
+
361
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden-active', 'onActive', false);
362
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-hidden-active', 'onInactive');
363
+ });
306
364
  });
307
365
 
308
366
  describe('renderTabs activeKey logic', () => {
367
+ const createTab = (uid: string, hidden = false) => ({
368
+ uid,
369
+ hidden,
370
+ context: {},
371
+ subModels: { grid: { mapSubModels: vi.fn() } },
372
+ renderChildren: vi.fn(() => uid),
373
+ });
374
+
375
+ const initialSyncState = {
376
+ previousEffectiveActiveKey: undefined,
377
+ previousAllHidden: false,
378
+ lastCorrectionSignature: undefined,
379
+ };
380
+
381
+ const runTabSync = (
382
+ result: { props: { children?: unknown } },
383
+ state: typeof initialSyncState | Record<string, unknown> = initialSyncState,
384
+ ) => {
385
+ const syncElement = getTabSyncElement(result);
386
+ expect(typeof syncElement?.props?.onSync).toBe('function');
387
+ return syncElement.props.onSync(state);
388
+ };
389
+
309
390
  beforeEach(() => {
310
391
  // Mock mapTabs to avoid complex rendering logic inside it
311
392
  pageModel.mapTabs = vi.fn().mockReturnValue([]);
@@ -329,7 +410,7 @@ describe('PageModel', () => {
329
410
 
330
411
  const result = pageModel.renderTabs() as any;
331
412
  // result is <DndProvider><Tabs ... /></DndProvider>
332
- const tabsElement = result.props.children;
413
+ const tabsElement = getTabsElement(result);
333
414
 
334
415
  expect(tabsElement.props.activeKey).toBe('tab-from-params');
335
416
  });
@@ -345,10 +426,589 @@ describe('PageModel', () => {
345
426
  };
346
427
 
347
428
  const result = pageModel.renderTabs() as any;
348
- const tabsElement = result.props.children;
429
+ const tabsElement = getTabsElement(result);
349
430
  expect(tabsElement.props.activeKey).toBe('first-tab-uid');
350
431
  });
351
432
 
433
+ it('should use the first visible tab when activeKey is unspecified', () => {
434
+ const hiddenTab = createTab('tab-hidden', true);
435
+ const visibleTab = createTab('tab-visible');
436
+ (pageModel as any).subModels = { tabs: [hiddenTab, visibleTab] };
437
+ pageModel.context.view = {
438
+ navigation: {
439
+ viewParams: {},
440
+ changeTo: vi.fn(),
441
+ },
442
+ } as any;
443
+
444
+ const tabsElement = getTabsElement(pageModel.renderTabs() as any);
445
+
446
+ expect(tabsElement.props.activeKey).toBe('tab-visible');
447
+ expect(pageModel.context.view.navigation.changeTo).not.toHaveBeenCalled();
448
+ });
449
+
450
+ it('should keep a visible URL activeKey unchanged', () => {
451
+ (pageModel as any).subModels = { tabs: [createTab('tab-1'), createTab('tab-2')] };
452
+ const changeTo = vi.fn();
453
+ pageModel.context.view = {
454
+ navigation: {
455
+ viewParams: { tabUid: 'tab-2' },
456
+ changeTo,
457
+ },
458
+ } as any;
459
+
460
+ const result = pageModel.renderTabs() as any;
461
+ const tabsElement = getTabsElement(result);
462
+ const nextState = runTabSync(result);
463
+
464
+ expect(tabsElement.props.activeKey).toBe('tab-2');
465
+ expect(nextState.effectiveActiveKey).toBe('tab-2');
466
+ expect(changeTo).not.toHaveBeenCalled();
467
+ });
468
+
469
+ it('should keep an unknown URL activeKey for the existing deep-link behavior', () => {
470
+ (pageModel as any).subModels = { tabs: [createTab('tab-1'), createTab('tab-2')] };
471
+ const changeTo = vi.fn();
472
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
473
+ pageModel.context.view = {
474
+ navigation: {
475
+ viewParams: { tabUid: 'unknown-tab' },
476
+ changeTo,
477
+ },
478
+ } as any;
479
+
480
+ const result = pageModel.renderTabs() as any;
481
+ const tabsElement = getTabsElement(result);
482
+ const nextState = runTabSync(result);
483
+
484
+ expect(tabsElement.props.activeKey).toBe('unknown-tab');
485
+ expect(nextState.effectiveActiveKey).toBe('unknown-tab');
486
+ expect(changeTo).not.toHaveBeenCalled();
487
+ expect(invokeSpy).not.toHaveBeenCalled();
488
+ });
489
+
490
+ it('should keep a hidden URL activeKey and render its content without exposing a tab item', () => {
491
+ (pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
492
+ pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
493
+ pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
494
+ const changeTo = vi.fn();
495
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
496
+ pageModel.context.view = {
497
+ navigation: {
498
+ viewParams: { tabUid: 'tab-hidden' },
499
+ changeTo,
500
+ },
501
+ } as any;
502
+
503
+ const result = pageModel.renderTabs() as any;
504
+ const tabsElement = getTabsElement(result);
505
+ const nextState = runTabSync(result, {
506
+ ...initialSyncState,
507
+ previousEffectiveActiveKey: 'tab-hidden',
508
+ });
509
+
510
+ expect(tabsElement.props.activeKey).toBe('tab-hidden');
511
+ expect(tabsElement.props.items.map((item: { key: string }) => item.key)).toEqual(['tab-hidden', 'tab-visible']);
512
+ expect(tabsElement.props.items[0].children).toBe('tab-hidden');
513
+ expect(getHiddenActiveTabContent(result)).toBeUndefined();
514
+ expect(typeof tabsElement.props.renderTabBar).toBe('function');
515
+ expect(changeTo).not.toHaveBeenCalled();
516
+ expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
517
+ expect(invokeSpy).not.toHaveBeenCalled();
518
+ expect(nextState.effectiveActiveKey).toBe('tab-hidden');
519
+ });
520
+
521
+ it('should keep a hidden explicit props activeKey and render its content without lifecycle switching', () => {
522
+ (pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
523
+ pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
524
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
525
+ pageModel.context.view = { navigation: null } as any;
526
+
527
+ const result = pageModel.renderTabs() as any;
528
+ const nextState = runTabSync(result, {
529
+ ...initialSyncState,
530
+ previousEffectiveActiveKey: 'tab-hidden',
531
+ });
532
+
533
+ expect(getTabsElement(result).props.activeKey).toBe('tab-hidden');
534
+ expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
535
+ expect(invokeSpy).not.toHaveBeenCalled();
536
+ expect(nextState.effectiveActiveKey).toBe('tab-hidden');
537
+ });
538
+
539
+ it('should not switch when a non-active tab becomes hidden', () => {
540
+ (pageModel as any).subModels = { tabs: [createTab('tab-active'), createTab('tab-hidden', true)] };
541
+ pageModel.props = { tabActiveKey: 'tab-active' } as any;
542
+ const changeTo = vi.fn();
543
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
544
+ pageModel.context.view = {
545
+ navigation: {
546
+ viewParams: { tabUid: 'tab-active' },
547
+ changeTo,
548
+ },
549
+ } as any;
550
+
551
+ const nextState = runTabSync(pageModel.renderTabs() as any, {
552
+ ...initialSyncState,
553
+ previousEffectiveActiveKey: 'tab-active',
554
+ });
555
+
556
+ expect(nextState.effectiveActiveKey).toBe('tab-active');
557
+ expect(changeTo).not.toHaveBeenCalled();
558
+ expect(invokeSpy).not.toHaveBeenCalled();
559
+ });
560
+
561
+ it('should switch an implicitly active tab when it becomes hidden without adding tabUid to URL', () => {
562
+ const oldTab = createTab('tab-old');
563
+ (pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
564
+ const changeTo = vi.fn();
565
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
566
+ const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
567
+ pageModel.context.view = {
568
+ navigation: {
569
+ viewParams: {},
570
+ changeTo,
571
+ },
572
+ } as any;
573
+
574
+ let syncState = runTabSync(pageModel.renderTabs() as any);
575
+ expect(pageModel.props.tabActiveKey).toBe('tab-old');
576
+
577
+ invokeSpy.mockClear();
578
+ updateTitleSpy.mockClear();
579
+ oldTab.hidden = true;
580
+ const result = pageModel.renderTabs() as any;
581
+ syncState = runTabSync(result, syncState);
582
+
583
+ expect(changeTo).not.toHaveBeenCalled();
584
+ expect(getTabsElement(result).props.activeKey).toBe('tab-new');
585
+ expect(pageModel.props.tabActiveKey).toBe('tab-new');
586
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
587
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
588
+ expect(updateTitleSpy).toHaveBeenCalledWith('tab-new', 0);
589
+ expect(syncState.effectiveActiveKey).toBe('tab-new');
590
+
591
+ invokeSpy.mockClear();
592
+ updateTitleSpy.mockClear();
593
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
594
+
595
+ expect(invokeSpy).not.toHaveBeenCalled();
596
+ expect(updateTitleSpy).not.toHaveBeenCalled();
597
+ expect(syncState.effectiveActiveKey).toBe('tab-new');
598
+ });
599
+
600
+ it('should switch the implicit default in UI Editor while keeping hidden tab items', () => {
601
+ const oldTab = createTab('tab-old');
602
+ (pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
603
+ pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
604
+ const changeTo = vi.fn();
605
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
606
+ const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
607
+ pageModel.context = {
608
+ t: (value: string) => value,
609
+ flowSettingsEnabled: true,
610
+ view: {
611
+ navigation: {
612
+ viewParams: {},
613
+ changeTo,
614
+ },
615
+ },
616
+ } as any;
617
+
618
+ let syncState = runTabSync(pageModel.renderTabs() as any);
619
+ invokeSpy.mockClear();
620
+ updateTitleSpy.mockClear();
621
+
622
+ oldTab.hidden = true;
623
+ const result = pageModel.renderTabs() as any;
624
+ syncState = runTabSync(result, syncState);
625
+
626
+ const tabsElement = getTabsElement(result);
627
+ expect(tabsElement.props.items.map((item: { key: string }) => item.key)).toEqual(['tab-old', 'tab-new']);
628
+ expect(tabsElement.props.activeKey).toBe('tab-new');
629
+ expect(changeTo).not.toHaveBeenCalled();
630
+ expect(pageModel.props.tabActiveKey).toBe('tab-new');
631
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
632
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
633
+ expect(updateTitleSpy).toHaveBeenCalledWith('tab-new', 0);
634
+ expect(syncState.effectiveActiveKey).toBe('tab-new');
635
+ });
636
+
637
+ it('should distinguish an implicit activeKey from explicit props without route navigation', () => {
638
+ const oldTab = createTab('tab-old');
639
+ (pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
640
+ pageModel.context.view = { navigation: null } as any;
641
+
642
+ let syncState = runTabSync(pageModel.renderTabs() as any);
643
+ expect(pageModel.props.tabActiveKey).toBe('tab-old');
644
+
645
+ oldTab.hidden = true;
646
+ const result = pageModel.renderTabs() as any;
647
+ syncState = runTabSync(result, syncState);
648
+
649
+ expect(getTabsElement(result).props.activeKey).toBe('tab-new');
650
+ expect(pageModel.props.tabActiveKey).toBe('tab-new');
651
+ expect(syncState.effectiveActiveKey).toBe('tab-new');
652
+ });
653
+
654
+ it('should preserve an explicit props update after establishing an implicit activeKey', () => {
655
+ const hiddenTab = createTab('tab-hidden', true);
656
+ (pageModel as any).subModels = { tabs: [createTab('tab-default'), hiddenTab] };
657
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
658
+ pageModel.context.view = { navigation: null } as any;
659
+
660
+ let syncState = runTabSync(pageModel.renderTabs() as any);
661
+ expect(pageModel.props.tabActiveKey).toBe('tab-default');
662
+
663
+ invokeSpy.mockClear();
664
+ pageModel.setProps('tabActiveKey', 'tab-hidden');
665
+ const result = pageModel.renderTabs() as any;
666
+ syncState = runTabSync(result, syncState);
667
+
668
+ expect(getTabsElement(result).props.activeKey).toBe('tab-hidden');
669
+ expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
670
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden', 'onActive');
671
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-default', 'onInactive');
672
+ expect(syncState.effectiveActiveKey).toBe('tab-hidden');
673
+ });
674
+
675
+ it('should treat an input tabUid as explicit when a page model mounts again', () => {
676
+ const oldTab = createTab('tab-old');
677
+ (pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
678
+ vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
679
+ pageModel.context.view = { navigation: null, inputArgs: {} } as any;
680
+
681
+ runTabSync(pageModel.renderTabs() as any);
682
+ expect(pageModel.props.tabActiveKey).toBe('tab-old');
683
+
684
+ oldTab.hidden = true;
685
+ pageModel.context.view.inputArgs.tabUid = 'tab-old';
686
+ pageModel.onMount();
687
+
688
+ expect(getTabsElement(pageModel.renderTabs() as any).props.activeKey).toBe('tab-old');
689
+ });
690
+
691
+ it('should replace an implicit activeKey when its tab no longer exists', () => {
692
+ const oldTab = createTab('tab-old');
693
+ const newTab = createTab('tab-new');
694
+ (pageModel as any).subModels = { tabs: [oldTab, newTab] };
695
+ (pageModel as any).flowEngine = { getModel: vi.fn(() => undefined) };
696
+ pageModel.context.view = {
697
+ navigation: {
698
+ viewParams: {},
699
+ changeTo: vi.fn(),
700
+ },
701
+ } as any;
702
+
703
+ let syncState = runTabSync(pageModel.renderTabs() as any);
704
+ (pageModel as any).subModels = { tabs: [newTab] };
705
+ const result = pageModel.renderTabs() as any;
706
+ syncState = runTabSync(result, syncState);
707
+
708
+ expect(getTabsElement(result).props.activeKey).toBe('tab-new');
709
+ expect(pageModel.props.tabActiveKey).toBe('tab-new');
710
+ expect(syncState.effectiveActiveKey).toBe('tab-new');
711
+ expect(pageModel.context.view.navigation.changeTo).not.toHaveBeenCalled();
712
+ });
713
+
714
+ it('should clear an implicit activeKey and restore the page title when its last tab no longer exists', async () => {
715
+ const oldTab = createTab('tab-old');
716
+ (pageModel as any).subModels = { tabs: [oldTab] };
717
+ pageModel.props = { enableTabs: true, title: 'Page title' } as any;
718
+ (pageModel as any).flowEngine = { context: {}, getModel: vi.fn(() => undefined) };
719
+ const changeTo = vi.fn();
720
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
721
+ const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle');
722
+ pageModel.context.view = {
723
+ navigation: {
724
+ viewParams: {},
725
+ changeTo,
726
+ },
727
+ } as any;
728
+
729
+ let syncState = runTabSync(pageModel.renderTabs() as any);
730
+ invokeSpy.mockClear();
731
+ updateTitleSpy.mockClear();
732
+
733
+ (pageModel as any).subModels = { tabs: [] };
734
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
735
+
736
+ expect(pageModel.props.tabActiveKey).toBeUndefined();
737
+ expect(changeTo).not.toHaveBeenCalled();
738
+ expect(invokeSpy).toHaveBeenCalledWith('tab-old', 'onInactive');
739
+ expect(updateTitleSpy).toHaveBeenCalledWith(undefined, 0);
740
+ expect(syncState.effectiveActiveKey).toBeUndefined();
741
+ await waitFor(() => expect(document.title).toBe('Page title'));
742
+ });
743
+
744
+ it('should synchronize an implicit hidden tab through the mounted effect', async () => {
745
+ const oldTab = createTab('tab-old');
746
+ (pageModel as any).subModels = { tabs: [oldTab, createTab('tab-new')] };
747
+ const changeTo = vi.fn();
748
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
749
+ const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
750
+ pageModel.context.view = {
751
+ navigation: {
752
+ viewParams: {},
753
+ changeTo,
754
+ },
755
+ } as any;
756
+
757
+ const view = render(pageModel.renderTabs() as any);
758
+ await waitFor(() => expect(pageModel.props.tabActiveKey).toBe('tab-old'));
759
+
760
+ invokeSpy.mockClear();
761
+ updateTitleSpy.mockClear();
762
+ oldTab.hidden = true;
763
+ view.rerender(pageModel.renderTabs() as any);
764
+
765
+ await waitFor(() => expect(pageModel.props.tabActiveKey).toBe('tab-new'));
766
+ expect(changeTo).not.toHaveBeenCalled();
767
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
768
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
769
+ expect(updateTitleSpy).toHaveBeenCalledWith('tab-new', 0);
770
+
771
+ invokeSpy.mockClear();
772
+ updateTitleSpy.mockClear();
773
+ view.rerender(pageModel.renderTabs() as any);
774
+ await waitFor(() => expect(pageModel.props.tabActiveKey).toBe('tab-new'));
775
+ expect(invokeSpy).not.toHaveBeenCalled();
776
+ expect(updateTitleSpy).not.toHaveBeenCalled();
777
+ });
778
+
779
+ it('should clear an implicit activeKey when all tabs become hidden', () => {
780
+ const firstTab = createTab('tab-1');
781
+ const secondTab = createTab('tab-2');
782
+ (pageModel as any).subModels = { tabs: [firstTab, secondTab] };
783
+ const changeTo = vi.fn();
784
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
785
+ const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
786
+ pageModel.context.view = {
787
+ navigation: {
788
+ viewParams: {},
789
+ changeTo,
790
+ },
791
+ } as any;
792
+
793
+ let syncState = runTabSync(pageModel.renderTabs() as any);
794
+ invokeSpy.mockClear();
795
+ updateTitleSpy.mockClear();
796
+
797
+ firstTab.hidden = true;
798
+ secondTab.hidden = true;
799
+ const result = pageModel.renderTabs() as any;
800
+ syncState = runTabSync(result, syncState);
801
+
802
+ expect(getTabsElement(result).props.activeKey).not.toBe('tab-1');
803
+ expect(getTabsElement(result).props.activeKey).not.toBe('tab-2');
804
+ expect(pageModel.props.tabActiveKey).toBeUndefined();
805
+ expect(changeTo).not.toHaveBeenCalled();
806
+ expect(invokeSpy).toHaveBeenCalledWith('tab-1', 'onInactive');
807
+ expect(updateTitleSpy).toHaveBeenCalledWith(undefined, 0);
808
+ expect(syncState).toMatchObject({ effectiveActiveKey: undefined, previousAllHidden: true });
809
+ });
810
+
811
+ it('should restore the implicit default when a tab becomes visible after all tabs were hidden', () => {
812
+ const firstTab = createTab('tab-1');
813
+ const secondTab = createTab('tab-2');
814
+ (pageModel as any).subModels = { tabs: [firstTab, secondTab] };
815
+ const changeTo = vi.fn();
816
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
817
+ pageModel.context.view = {
818
+ navigation: {
819
+ viewParams: {},
820
+ changeTo,
821
+ },
822
+ } as any;
823
+
824
+ let syncState = runTabSync(pageModel.renderTabs() as any);
825
+ firstTab.hidden = true;
826
+ secondTab.hidden = true;
827
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
828
+ invokeSpy.mockClear();
829
+
830
+ secondTab.hidden = false;
831
+ const result = pageModel.renderTabs() as any;
832
+ syncState = runTabSync(result, syncState);
833
+
834
+ expect(getTabsElement(result).props.activeKey).toBe('tab-2');
835
+ expect(pageModel.props.tabActiveKey).toBe('tab-2');
836
+ expect(changeTo).not.toHaveBeenCalled();
837
+ expect(invokeSpy).toHaveBeenCalledWith('tab-2', 'onActive');
838
+ expect(syncState).toMatchObject({ effectiveActiveKey: 'tab-2', previousAllHidden: false });
839
+ });
840
+
841
+ it('should keep an explicitly requested activeKey when all tabs are hidden', () => {
842
+ (pageModel as any).subModels = { tabs: [createTab('tab-1', true), createTab('tab-2', true)] };
843
+ pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
844
+ pageModel.props = { tabActiveKey: 'tab-1' } as any;
845
+ const changeTo = vi.fn();
846
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
847
+ pageModel.context.view = {
848
+ navigation: {
849
+ viewParams: { tabUid: 'tab-1' },
850
+ changeTo,
851
+ },
852
+ } as any;
853
+
854
+ const result = pageModel.renderTabs() as any;
855
+ const nextState = runTabSync(result, {
856
+ ...initialSyncState,
857
+ previousEffectiveActiveKey: 'tab-1',
858
+ });
859
+
860
+ expect(getTabsElement(result).props.activeKey).toBe('tab-1');
861
+ expect(getTabsElement(result).props.items.map((item: { key: string }) => item.key)).toEqual(['tab-1', 'tab-2']);
862
+ expect(getTabsElement(result).props.items[0].children).toBe('tab-1');
863
+ expect(getHiddenActiveTabContent(result)).toBeUndefined();
864
+ expect(changeTo).not.toHaveBeenCalled();
865
+ expect(pageModel.props.tabActiveKey).toBe('tab-1');
866
+ expect(invokeSpy).not.toHaveBeenCalled();
867
+ expect(nextState).toMatchObject({ effectiveActiveKey: 'tab-1', previousAllHidden: true });
868
+ });
869
+
870
+ it('should not implicitly activate a hidden pane when all entries are hidden and activeKey is unspecified', () => {
871
+ (pageModel as any).subModels = { tabs: [createTab('tab-1', true), createTab('tab-2', true)] };
872
+ pageModel.mapTabs = PageModelClass.prototype.mapTabs.bind(pageModel);
873
+ pageModel.context.view = {
874
+ navigation: {
875
+ viewParams: {},
876
+ changeTo: vi.fn(),
877
+ },
878
+ } as any;
879
+
880
+ const tabsElement = getTabsElement(pageModel.renderTabs() as any);
881
+
882
+ expect(tabsElement.props.items.map((item: { key: string }) => item.key)).toEqual(['tab-1', 'tab-2']);
883
+ expect(tabsElement.props.activeKey).not.toBeUndefined();
884
+ expect(tabsElement.props.activeKey).not.toBe('tab-1');
885
+ expect(tabsElement.props.activeKey).not.toBe('tab-2');
886
+ expect(pageModel.context.view.navigation.changeTo).not.toHaveBeenCalled();
887
+ });
888
+
889
+ it('should keep the requested hidden tab active when other tab entries become visible again', () => {
890
+ const firstTab = createTab('tab-1', true);
891
+ const secondTab = createTab('tab-2', true);
892
+ (pageModel as any).subModels = { tabs: [firstTab, secondTab] };
893
+ pageModel.props = { tabActiveKey: 'tab-1' } as any;
894
+ const changeTo = vi.fn();
895
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
896
+ vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
897
+ pageModel.context.view = {
898
+ navigation: {
899
+ viewParams: { tabUid: 'tab-1' },
900
+ changeTo,
901
+ },
902
+ } as any;
903
+
904
+ let syncState = runTabSync(pageModel.renderTabs() as any, {
905
+ ...initialSyncState,
906
+ previousEffectiveActiveKey: 'tab-1',
907
+ });
908
+ secondTab.hidden = false;
909
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
910
+
911
+ expect(changeTo).not.toHaveBeenCalled();
912
+ expect(pageModel.props.tabActiveKey).toBe('tab-1');
913
+ expect(invokeSpy).not.toHaveBeenCalled();
914
+ expect(syncState.effectiveActiveKey).toBe('tab-1');
915
+
916
+ firstTab.hidden = false;
917
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
918
+
919
+ expect(syncState.effectiveActiveKey).toBe('tab-1');
920
+ expect(pageModel.props.tabActiveKey).toBe('tab-1');
921
+ expect(changeTo).not.toHaveBeenCalled();
922
+ expect(invokeSpy).not.toHaveBeenCalled();
923
+ });
924
+
925
+ it('should keep the hidden active tab after UI Editor closes', () => {
926
+ (pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
927
+ pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
928
+ const changeTo = vi.fn();
929
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
930
+ pageModel.context = {
931
+ t: (value: string) => value,
932
+ flowSettingsEnabled: true,
933
+ view: {
934
+ navigation: {
935
+ viewParams: { tabUid: 'tab-hidden' },
936
+ changeTo,
937
+ },
938
+ },
939
+ } as any;
940
+
941
+ let syncState = runTabSync(pageModel.renderTabs() as any, {
942
+ ...initialSyncState,
943
+ previousEffectiveActiveKey: 'tab-hidden',
944
+ });
945
+ expect(changeTo).not.toHaveBeenCalled();
946
+
947
+ pageModel.context.flowSettingsEnabled = false;
948
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
949
+
950
+ expect(syncState.effectiveActiveKey).toBe('tab-hidden');
951
+ expect(changeTo).not.toHaveBeenCalled();
952
+ expect(invokeSpy).not.toHaveBeenCalled();
953
+ });
954
+
955
+ it('should keep hidden active synchronization idempotent across repeated effect passes', () => {
956
+ (pageModel as any).subModels = { tabs: [createTab('tab-hidden', true), createTab('tab-visible')] };
957
+ pageModel.props = { tabActiveKey: 'tab-hidden' } as any;
958
+ const changeTo = vi.fn();
959
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
960
+ pageModel.context.view = {
961
+ navigation: {
962
+ viewParams: { tabUid: 'tab-hidden' },
963
+ changeTo,
964
+ },
965
+ } as any;
966
+
967
+ let syncState = runTabSync(pageModel.renderTabs() as any, {
968
+ ...initialSyncState,
969
+ previousEffectiveActiveKey: 'tab-hidden',
970
+ });
971
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
972
+
973
+ expect(syncState.effectiveActiveKey).toBe('tab-hidden');
974
+ expect(changeTo).not.toHaveBeenCalled();
975
+ expect(invokeSpy).not.toHaveBeenCalled();
976
+ });
977
+
978
+ it('should switch lifecycle and title when URL navigation selects a hidden tab', () => {
979
+ (pageModel as any).subModels = { tabs: [createTab('tab-visible'), createTab('tab-hidden', true)] };
980
+ pageModel.props = { tabActiveKey: 'tab-visible' } as any;
981
+ const changeTo = vi.fn();
982
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod');
983
+ const updateTitleSpy = vi.spyOn(pageModel as any, 'updateDocumentTitle').mockResolvedValue(undefined);
984
+ pageModel.context.view = {
985
+ navigation: {
986
+ viewParams: { tabUid: 'tab-hidden' },
987
+ changeTo,
988
+ },
989
+ } as any;
990
+
991
+ let syncState = runTabSync(pageModel.renderTabs() as any, {
992
+ ...initialSyncState,
993
+ previousEffectiveActiveKey: 'tab-visible',
994
+ });
995
+
996
+ expect(changeTo).not.toHaveBeenCalled();
997
+ expect(pageModel.props.tabActiveKey).toBe('tab-hidden');
998
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-hidden', 'onActive');
999
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-visible', 'onInactive');
1000
+ expect(updateTitleSpy).toHaveBeenCalledWith('tab-hidden', 0);
1001
+ expect(syncState.effectiveActiveKey).toBe('tab-hidden');
1002
+
1003
+ invokeSpy.mockClear();
1004
+ updateTitleSpy.mockClear();
1005
+ syncState = runTabSync(pageModel.renderTabs() as any, syncState);
1006
+
1007
+ expect(invokeSpy).not.toHaveBeenCalled();
1008
+ expect(updateTitleSpy).not.toHaveBeenCalled();
1009
+ expect(syncState.effectiveActiveKey).toBe('tab-hidden');
1010
+ });
1011
+
352
1012
  it('should use props.tabActiveKey if viewParams is missing', () => {
353
1013
  pageModel.props = { tabActiveKey: 'tab-from-props' } as any;
354
1014
  // @ts-ignore
@@ -357,7 +1017,7 @@ describe('PageModel', () => {
357
1017
  };
358
1018
 
359
1019
  const result = pageModel.renderTabs() as any;
360
- const tabsElement = result.props.children;
1020
+ const tabsElement = getTabsElement(result);
361
1021
  expect(tabsElement.props.activeKey).toBe('tab-from-props');
362
1022
  });
363
1023
 
@@ -370,7 +1030,7 @@ describe('PageModel', () => {
370
1030
  } as any;
371
1031
 
372
1032
  const result = pageModel.renderTabs() as any;
373
- const tabsElement = result.props.children;
1033
+ const tabsElement = getTabsElement(result);
374
1034
 
375
1035
  expect(tabsElement.props.className).toBeUndefined();
376
1036
  });
@@ -384,7 +1044,7 @@ describe('PageModel', () => {
384
1044
  } as any;
385
1045
 
386
1046
  const result = pageModel.renderTabs() as any;
387
- const tabsElement = result.props.children;
1047
+ const tabsElement = getTabsElement(result);
388
1048
 
389
1049
  expect(tabsElement.props.className).toBeUndefined();
390
1050
  });
@@ -398,7 +1058,7 @@ describe('PageModel', () => {
398
1058
  } as any;
399
1059
 
400
1060
  const result = pageModel.renderTabs() as any;
401
- const tabsElement = result.props.children;
1061
+ const tabsElement = getTabsElement(result);
402
1062
 
403
1063
  expect(tabsElement.props.tabBarExtraContent.left).toBeTruthy();
404
1064
  });
@@ -413,7 +1073,7 @@ describe('PageModel', () => {
413
1073
  pageModel.tabBarExtraContent = { left: 'back' } as any;
414
1074
 
415
1075
  const result = pageModel.renderTabs() as any;
416
- const tabsElement = result.props.children;
1076
+ const tabsElement = getTabsElement(result);
417
1077
 
418
1078
  expect(tabsElement.props.tabBarExtraContent.left).toBe('back');
419
1079
  });
@@ -428,7 +1088,7 @@ describe('PageModel', () => {
428
1088
  pageModel.tabBarExtraContent = { right: 'custom-right' } as any;
429
1089
 
430
1090
  const result = pageModel.renderTabs() as any;
431
- const tabsElement = result.props.children;
1091
+ const tabsElement = getTabsElement(result);
432
1092
 
433
1093
  expect(tabsElement.props.tabBarExtraContent.right).toBe('custom-right');
434
1094
  });
@@ -443,7 +1103,7 @@ describe('PageModel', () => {
443
1103
  } as any;
444
1104
 
445
1105
  const result = pageModel.renderTabs() as any;
446
- const tabsElement = result.props.children;
1106
+ const tabsElement = getTabsElement(result);
447
1107
  const rightExtraContent = tabsElement.props.tabBarExtraContent.right;
448
1108
 
449
1109
  expect(rightExtraContent).toBeTruthy();
@@ -452,6 +1112,57 @@ describe('PageModel', () => {
452
1112
  marginInlineEnd: 24,
453
1113
  });
454
1114
  });
1115
+
1116
+ it('should deactivate the previous tab when navigation updates active tab synchronously', () => {
1117
+ pageModel.props = { tabActiveKey: 'tab-old' } as any;
1118
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
1119
+ const changeTo = vi.fn((params: { tabUid: string }) => {
1120
+ pageModel.props.tabActiveKey = params.tabUid;
1121
+ });
1122
+
1123
+ pageModel.context.view = {
1124
+ navigation: {
1125
+ viewParams: {
1126
+ tabUid: 'tab-old',
1127
+ },
1128
+ changeTo,
1129
+ },
1130
+ } as any;
1131
+
1132
+ const result = pageModel.renderTabs() as any;
1133
+ const tabsElement = getTabsElement(result);
1134
+
1135
+ tabsElement.props.onChange('tab-new');
1136
+
1137
+ expect(changeTo).toHaveBeenCalledWith({ tabUid: 'tab-new' });
1138
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
1139
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
1140
+ expect(invokeSpy).not.toHaveBeenCalledWith('tab-new', 'onInactive');
1141
+ expect(pageModel.props.tabActiveKey).toBe('tab-new');
1142
+ });
1143
+
1144
+ it('should not repeat lifecycle when synchronization observes a locally committed tab change', () => {
1145
+ (pageModel as any).subModels = { tabs: [createTab('tab-old'), createTab('tab-new')] };
1146
+ pageModel.props = { tabActiveKey: 'tab-old' } as any;
1147
+ pageModel.context.view = { navigation: null } as any;
1148
+ const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
1149
+
1150
+ let result = pageModel.renderTabs() as any;
1151
+ let syncState = runTabSync(result, {
1152
+ ...initialSyncState,
1153
+ previousEffectiveActiveKey: 'tab-old',
1154
+ });
1155
+ getTabsElement(result).props.onChange('tab-new');
1156
+
1157
+ expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
1158
+ expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
1159
+
1160
+ result = pageModel.renderTabs() as any;
1161
+ syncState = runTabSync(result, syncState);
1162
+
1163
+ expect(invokeSpy).toHaveBeenCalledTimes(2);
1164
+ expect(syncState.effectiveActiveKey).toBe('tab-new');
1165
+ });
455
1166
  });
456
1167
 
457
1168
  describe('render header spacing with tabs', () => {
@@ -769,6 +1480,99 @@ describe('PageModel', () => {
769
1480
  expect(document.title).toBe('Fallback tab title');
770
1481
  });
771
1482
 
1483
+ it('should use the hidden active tab documentTitle when its entry is hidden', async () => {
1484
+ pageModel.props = { enableTabs: true, tabActiveKey: 'tab-hidden' } as any;
1485
+ const hiddenTab = {
1486
+ uid: 'tab-hidden',
1487
+ hidden: true,
1488
+ stepParams: {
1489
+ pageTabSettings: {
1490
+ tab: {
1491
+ documentTitle: 'Hidden tab document title',
1492
+ },
1493
+ },
1494
+ },
1495
+ getTabTitle: vi.fn(() => 'Hidden tab'),
1496
+ };
1497
+ const visibleTab = {
1498
+ uid: 'tab-visible',
1499
+ stepParams: {
1500
+ pageTabSettings: {
1501
+ tab: {
1502
+ documentTitle: 'Visible tab document title',
1503
+ },
1504
+ },
1505
+ },
1506
+ getTabTitle: vi.fn(() => 'Visible tab'),
1507
+ };
1508
+ (pageModel as any).subModels = { tabs: [hiddenTab, visibleTab] };
1509
+ (pageModel as any).context.view.navigation = {
1510
+ viewParams: { tabUid: 'tab-hidden' },
1511
+ };
1512
+
1513
+ await (pageModel as any).updateDocumentTitle();
1514
+
1515
+ expect(document.title).toBe('Hidden tab document title');
1516
+ });
1517
+
1518
+ it('should use page documentTitle when tabs are enabled but all tabs are hidden', async () => {
1519
+ pageModel.props = { enableTabs: true, title: 'Page title' } as any;
1520
+ const hiddenTab = {
1521
+ uid: 'tab-hidden',
1522
+ hidden: true,
1523
+ stepParams: {
1524
+ pageTabSettings: {
1525
+ tab: {
1526
+ documentTitle: 'Hidden tab title',
1527
+ },
1528
+ },
1529
+ },
1530
+ getTabTitle: vi.fn(() => 'Hidden tab'),
1531
+ };
1532
+ (pageModel as any).subModels = { tabs: [hiddenTab] };
1533
+ (pageModel as any).stepParams = {
1534
+ pageSettings: {
1535
+ general: {
1536
+ documentTitle: 'Page document title',
1537
+ },
1538
+ },
1539
+ };
1540
+ (pageModel as any).flowEngine.getModel = vi.fn(() => hiddenTab);
1541
+
1542
+ await (pageModel as any).updateDocumentTitle();
1543
+
1544
+ expect(document.title).toBe('Page document title');
1545
+ });
1546
+
1547
+ it('should fallback to page title when all tabs are hidden and page documentTitle is empty', async () => {
1548
+ pageModel.props = { enableTabs: true, title: 'Fallback page title' } as any;
1549
+ const hiddenTab = {
1550
+ uid: 'tab-hidden',
1551
+ hidden: true,
1552
+ stepParams: {
1553
+ pageTabSettings: {
1554
+ tab: {
1555
+ documentTitle: 'Hidden tab title',
1556
+ },
1557
+ },
1558
+ },
1559
+ getTabTitle: vi.fn(() => 'Hidden tab'),
1560
+ };
1561
+ (pageModel as any).subModels = { tabs: [hiddenTab] };
1562
+ (pageModel as any).stepParams = {
1563
+ pageSettings: {
1564
+ general: {
1565
+ documentTitle: '',
1566
+ },
1567
+ },
1568
+ };
1569
+ (pageModel as any).flowEngine.getModel = vi.fn(() => hiddenTab);
1570
+
1571
+ await (pageModel as any).updateDocumentTitle();
1572
+
1573
+ expect(document.title).toBe('Fallback page title');
1574
+ });
1575
+
772
1576
  it('should also update document title for closable page (popup)', async () => {
773
1577
  pageModel.props = { enableTabs: false, title: 'Popup page title' } as any;
774
1578
  (pageModel as any).context = {