@nocobase/client-v2 2.2.0-beta.1 → 2.2.0-beta.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 (252) hide show
  1. package/es/BaseApplication.d.ts +3 -0
  2. package/es/PluginSettingsManager.d.ts +33 -0
  3. package/es/RouteRepository.d.ts +29 -6
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/form/JsonTextArea.d.ts +2 -1
  9. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +1 -0
  12. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  13. package/es/entry-actions/index.d.ts +9 -0
  14. package/es/flow/actions/afterSuccess.d.ts +8 -1
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  17. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  19. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  21. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  22. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  23. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  24. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  25. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  26. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  27. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  28. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  29. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  30. package/es/flow/models/base/GridModel.d.ts +1 -1
  31. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  32. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  33. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  34. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  35. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  36. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +30 -0
  37. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  38. package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
  39. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  40. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  41. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  42. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  43. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  44. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  45. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  46. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  47. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  48. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  49. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  50. package/es/flow/utils/formValueDeps.d.ts +32 -0
  51. package/es/flow-compat/FieldValidation.d.ts +2 -1
  52. package/es/index.d.ts +4 -1
  53. package/es/index.mjs +315 -167
  54. package/es/utils/markdownSanitize.d.ts +11 -0
  55. package/lib/index.js +315 -167
  56. package/package.json +8 -7
  57. package/src/BaseApplication.tsx +12 -5
  58. package/src/PluginSettingsManager.ts +53 -0
  59. package/src/RouteRepository.ts +151 -24
  60. package/src/RouterManager.tsx +142 -1
  61. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  62. package/src/__tests__/RouteRepository.test.ts +239 -0
  63. package/src/__tests__/RouterManager.test.ts +125 -0
  64. package/src/__tests__/app.test.tsx +55 -0
  65. package/src/__tests__/authRedirect.test.ts +17 -0
  66. package/src/__tests__/exports.test.ts +16 -0
  67. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  68. package/src/__tests__/plugin-manager.test.tsx +44 -2
  69. package/src/__tests__/settings-center.test.tsx +40 -1
  70. package/src/authRedirect.ts +38 -0
  71. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  72. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  73. package/src/collection-manager/field-configure.ts +1 -1
  74. package/src/collection-manager/interfaces/id.ts +1 -1
  75. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  76. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  77. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  78. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  79. package/src/collection-manager/interfaces/obo.tsx +2 -2
  80. package/src/collection-manager/interfaces/oho.tsx +2 -2
  81. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  82. package/src/collection-manager/interfaces/uuid.ts +1 -1
  83. package/src/components/AppComponents.tsx +19 -3
  84. package/src/components/form/JsonTextArea.tsx +21 -11
  85. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
  86. package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
  87. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  88. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  89. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  90. package/src/components/form/index.tsx +1 -0
  91. package/src/components/index.ts +1 -0
  92. package/src/entry-actions/EntryActionManager.ts +76 -0
  93. package/src/entry-actions/index.ts +10 -0
  94. package/src/flow/FlowPage.tsx +38 -3
  95. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  96. package/src/flow/__tests__/FlowRoute.test.tsx +1088 -105
  97. package/src/flow/__tests__/getKey.test.ts +7 -0
  98. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  99. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  100. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +120 -0
  101. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  102. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  103. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  104. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  105. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  106. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  107. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
  108. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  109. package/src/flow/actions/afterSuccess.tsx +142 -3
  110. package/src/flow/actions/customVariable.tsx +1 -2
  111. package/src/flow/actions/dataScopeFilter.ts +20 -1
  112. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  113. package/src/flow/actions/index.ts +1 -1
  114. package/src/flow/actions/linkageRules.tsx +123 -35
  115. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  116. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  117. package/src/flow/actions/openView.tsx +59 -5
  118. package/src/flow/actions/runjs.tsx +2 -14
  119. package/src/flow/actions/validation.tsx +62 -30
  120. package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
  121. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
  122. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +74 -13
  124. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +486 -0
  125. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +64 -8
  126. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  127. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  128. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  129. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  130. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  131. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  132. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  133. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  134. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  135. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  136. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +518 -2
  137. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +214 -2
  138. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  139. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  140. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  141. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  142. package/src/flow/common/Markdown/Display.tsx +14 -3
  143. package/src/flow/common/Markdown/Edit.tsx +19 -7
  144. package/src/flow/components/DefaultValue.tsx +1 -2
  145. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  146. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  147. package/src/flow/components/FieldAssignValueInput.tsx +15 -11
  148. package/src/flow/components/FlowRoute.tsx +186 -25
  149. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  150. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  151. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
  152. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  153. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  154. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  155. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  156. package/src/flow/components/fieldAssignOptions.ts +155 -27
  157. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  158. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  159. package/src/flow/index.ts +1 -1
  160. package/src/flow/internal/components/Markdown/util.ts +2 -1
  161. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  162. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  163. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  164. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  165. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  166. package/src/flow/models/base/ActionModel.tsx +21 -8
  167. package/src/flow/models/base/ActionModelCore.tsx +11 -4
  168. package/src/flow/models/base/GridModel.tsx +38 -7
  169. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  170. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  171. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  172. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
  173. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  174. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  175. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +32 -3
  176. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  177. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  178. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  179. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +111 -0
  180. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  181. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  182. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  183. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  184. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  185. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  186. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  187. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  188. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  189. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  190. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  191. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +940 -75
  192. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  193. package/src/flow/models/blocks/form/value-runtime/runtime.ts +348 -13
  194. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  195. package/src/flow/models/blocks/js-block/JSBlock.tsx +225 -9
  196. package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
  197. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  198. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +38 -14
  199. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  200. package/src/flow/models/blocks/table/TableColumnModel.tsx +42 -5
  201. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  202. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  203. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  204. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  205. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +210 -1
  206. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  207. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  208. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  209. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  210. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +30 -6
  211. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +61 -17
  212. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  213. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  214. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  215. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  216. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  217. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  218. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  219. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  220. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  221. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  222. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  223. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  224. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  225. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  226. package/src/flow/models/fields/TextareaFieldModel.tsx +72 -3
  227. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  228. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  229. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  230. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +131 -0
  231. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  232. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  233. package/src/flow/models/topbar/TopbarActionModel.tsx +168 -10
  234. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  235. package/src/flow/routeTransientInputArgs.ts +27 -0
  236. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +300 -0
  237. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  238. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  239. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  240. package/src/flow/utils/formValueDeps.ts +170 -0
  241. package/src/flow-compat/FieldValidation.tsx +122 -60
  242. package/src/flow-compat/Popover.tsx +43 -4
  243. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  244. package/src/index.ts +10 -1
  245. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  246. package/src/nocobase-buildin-plugin/index.tsx +51 -15
  247. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  248. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  249. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  250. package/src/settings-center/plugin-manager/index.tsx +3 -0
  251. package/src/settings-center/utils.tsx +0 -6
  252. package/src/utils/markdownSanitize.ts +88 -0
@@ -9,10 +9,12 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
- import { MemoryRouter, Route, Routes } from 'react-router-dom';
13
- import { render, screen, waitFor } from '@testing-library/react';
12
+ import { MemoryRouter, Route, Router, Routes, useNavigate } from 'react-router-dom';
13
+ import { act, render, screen, waitFor } from '@testing-library/react';
14
14
  import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
15
15
  import FlowRoute from '../components/FlowRoute';
16
+ import { RouteRepository } from '../../RouteRepository';
17
+ import { NocoBaseDesktopRouteType } from '../../flow-compat';
16
18
 
17
19
  type MockAdminLayoutModel = FlowModel & {
18
20
  registerRoutePage: ReturnType<typeof vi.fn>;
@@ -20,6 +22,8 @@ type MockAdminLayoutModel = FlowModel & {
20
22
  unregisterRoutePage: ReturnType<typeof vi.fn>;
21
23
  };
22
24
 
25
+ type RouterNavigator = React.ComponentProps<typeof Router>['navigator'];
26
+
23
27
  const { hookState } = vi.hoisted(() => {
24
28
  return {
25
29
  hookState: {
@@ -321,6 +325,94 @@ describe('FlowRoute', () => {
321
325
  });
322
326
  });
323
327
 
328
+ it('should ensure accessible routes for the current layout before bridging', async () => {
329
+ const engine = new FlowEngine();
330
+ const request = vi.fn().mockResolvedValue({
331
+ data: {
332
+ data: [
333
+ {
334
+ type: 'flowPage',
335
+ schemaUid: 'mobile-page',
336
+ },
337
+ ],
338
+ },
339
+ });
340
+ const routeRepository = new RouteRepository({
341
+ api: {
342
+ request,
343
+ resource: vi.fn(),
344
+ },
345
+ } as never);
346
+ routeRepository.setRoutes([], 'admin-layout-model');
347
+ const deactivateLayout = routeRepository.activateLayout({
348
+ uid: 'mobile-layout-model',
349
+ });
350
+ engine.context.defineProperty('routeRepository', {
351
+ value: routeRepository,
352
+ });
353
+ engine.context.defineProperty('app', {
354
+ value: {
355
+ getPublicPath: () => '/v2/',
356
+ router: {
357
+ getBasename: () => '/v2',
358
+ },
359
+ },
360
+ });
361
+ const routeModel = engine.createModel({
362
+ uid: 'mobile-route-model',
363
+ use: 'FlowModel',
364
+ });
365
+ routeModel.context.defineProperty('layout', {
366
+ value: {
367
+ routeName: 'mobile',
368
+ routePath: '/mobile',
369
+ rootRouteName: 'mobile',
370
+ uid: 'mobile-layout-model',
371
+ layoutModelClass: 'MobileLayoutModel',
372
+ rootPageModelClass: 'MobileRootPageModel',
373
+ childPageModelClass: 'MobileChildPageModel',
374
+ authCheck: true,
375
+ },
376
+ });
377
+
378
+ const mobileLayoutModel: MockAdminLayoutModel = Object.assign(
379
+ engine.createModel({
380
+ uid: 'mobile-layout-model',
381
+ use: 'FlowModel',
382
+ }),
383
+ {
384
+ registerRoutePage: vi.fn(),
385
+ updateRoutePage: vi.fn(),
386
+ unregisterRoutePage: vi.fn(),
387
+ },
388
+ );
389
+
390
+ render(
391
+ <FlowEngineProvider engine={engine}>
392
+ <FlowContextProvider context={routeModel.context}>
393
+ <MemoryRouter initialEntries={['/mobile/mobile-page']}>
394
+ <Routes>
395
+ <Route path="/mobile/:name" element={<FlowRoute />} />
396
+ </Routes>
397
+ </MemoryRouter>
398
+ </FlowContextProvider>
399
+ </FlowEngineProvider>,
400
+ );
401
+
402
+ await waitFor(() => {
403
+ expect(request).toHaveBeenCalledWith({
404
+ url: '/desktopRoutes:listAccessible',
405
+ params: {
406
+ tree: true,
407
+ sort: 'sort',
408
+ layout: 'mobile-layout-model',
409
+ },
410
+ });
411
+ expect(mobileLayoutModel.registerRoutePage).toHaveBeenCalledWith('mobile-page', expect.any(Object));
412
+ });
413
+ deactivateLayout();
414
+ });
415
+
324
416
  it('should fail fast when admin-layout-model is missing', () => {
325
417
  const engine = new FlowEngine();
326
418
  engine.context.defineProperty('route', {
@@ -420,6 +512,75 @@ describe('FlowRoute', () => {
420
512
  });
421
513
  });
422
514
 
515
+ it('should not rerun accessible guard when layout reference changes but fields stay the same', async () => {
516
+ const engine = new FlowEngine();
517
+ const ensureAccessibleLoaded = vi.fn().mockResolvedValue([]);
518
+ const getRouteBySchemaUid = vi.fn(() => ({ type: 'flowPage', schemaUid: 'test-page' }));
519
+ engine.context.defineProperty('routeRepository', {
520
+ value: {
521
+ refreshAccessible: hookState.refresh,
522
+ isAccessibleLoaded: () => true,
523
+ ensureAccessibleLoaded,
524
+ getRouteBySchemaUid,
525
+ },
526
+ });
527
+ engine.context.defineProperty('app', {
528
+ value: {
529
+ getPublicPath: () => '/v2/',
530
+ router: {
531
+ getBasename: () => '/v2',
532
+ },
533
+ },
534
+ });
535
+
536
+ const layoutModel: MockAdminLayoutModel = Object.assign(
537
+ engine.createModel({ uid: 'legacy-admin-layout-model', use: 'FlowModel' }),
538
+ {
539
+ registerRoutePage: vi.fn(),
540
+ updateRoutePage: vi.fn(),
541
+ unregisterRoutePage: vi.fn(),
542
+ },
543
+ );
544
+ let layoutReadCount = 0;
545
+ const stableLayout = {
546
+ uid: 'admin-layout-model',
547
+ routeName: 'admin',
548
+ routePath: '/admin',
549
+ authCheck: true,
550
+ };
551
+ Object.defineProperty(layoutModel, 'layout', {
552
+ get: () => {
553
+ layoutReadCount += 1;
554
+ return layoutReadCount <= 2 ? { ...stableLayout } : stableLayout;
555
+ },
556
+ });
557
+
558
+ const element = (
559
+ <FlowEngineProvider engine={engine}>
560
+ <MemoryRouter initialEntries={['/flow/test-page']}>
561
+ <Routes>
562
+ <Route path="/flow/:name" element={<FlowRoute getLayoutModel={() => layoutModel as any} />} />
563
+ </Routes>
564
+ </MemoryRouter>
565
+ </FlowEngineProvider>
566
+ );
567
+ const { rerender } = render(element);
568
+
569
+ await waitFor(() => {
570
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('test-page', expect.any(Object));
571
+ });
572
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
573
+ expect(getRouteBySchemaUid).toHaveBeenCalledTimes(1);
574
+
575
+ rerender(element);
576
+
577
+ await waitFor(() => {
578
+ expect(layoutModel.updateRoutePage).toHaveBeenCalled();
579
+ });
580
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
581
+ expect(getRouteBySchemaUid).toHaveBeenCalledTimes(1);
582
+ });
583
+
423
584
  it('should show 404 when current route is a legacy page in v2 runtime', async () => {
424
585
  const originalLocation = window.location;
425
586
  const replace = vi.fn();
@@ -571,6 +732,22 @@ describe('FlowRoute', () => {
571
732
  },
572
733
  },
573
734
  });
735
+ const routeModel = engine.createModel({
736
+ uid: 'public-form-route-model',
737
+ use: 'FlowModel',
738
+ });
739
+ routeModel.context.defineProperty('layout', {
740
+ value: {
741
+ routeName: 'public-forms',
742
+ routePath: '/public-forms',
743
+ rootRouteName: 'public-forms',
744
+ uid: 'public-form-layout-model',
745
+ layoutModelClass: 'PublicFormLayoutModel',
746
+ rootPageModelClass: 'PublicFormPageModel',
747
+ childPageModelClass: 'ChildPageModel',
748
+ authCheck: false,
749
+ },
750
+ });
574
751
 
575
752
  const layoutModel: MockAdminLayoutModel = Object.assign(
576
753
  engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
@@ -583,14 +760,16 @@ describe('FlowRoute', () => {
583
760
 
584
761
  render(
585
762
  <FlowEngineProvider engine={engine}>
586
- <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
587
- <Routes>
588
- <Route
589
- path="/public-forms/:name"
590
- element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
591
- />
592
- </Routes>
593
- </MemoryRouter>
763
+ <FlowContextProvider context={routeModel.context}>
764
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
765
+ <Routes>
766
+ <Route
767
+ path="/public-forms/:name"
768
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
769
+ />
770
+ </Routes>
771
+ </MemoryRouter>
772
+ </FlowContextProvider>
594
773
  </FlowEngineProvider>,
595
774
  );
596
775
 
@@ -600,20 +779,13 @@ describe('FlowRoute', () => {
600
779
  expect(screen.queryByText('404')).not.toBeInTheDocument();
601
780
  });
602
781
 
603
- it('should check model existence without occupying the route model uid', async () => {
782
+ it('should use getLayoutModel layout authCheck when FlowContext has no layout', async () => {
604
783
  const engine = new FlowEngine();
605
- const findOne = vi.fn().mockResolvedValue({
606
- uid: 'public-form-1',
607
- });
608
- const request = vi.fn().mockResolvedValue({
609
- data: {
610
- data: {
611
- uid: 'public-form-1',
612
- },
613
- },
614
- });
615
784
  engine.setModelRepository({
616
- findOne,
785
+ findOne: vi.fn().mockResolvedValue({
786
+ uid: 'public-form-1',
787
+ use: 'FlowModel',
788
+ }),
617
789
  save: vi.fn(),
618
790
  destroy: vi.fn(),
619
791
  } as any);
@@ -627,20 +799,22 @@ describe('FlowRoute', () => {
627
799
  });
628
800
  engine.context.defineProperty('app', {
629
801
  value: {
630
- apiClient: {
631
- request,
632
- },
633
802
  getPublicPath: () => '/v2/',
634
803
  router: {
635
804
  getBasename: () => '/v2',
636
805
  },
637
806
  },
638
807
  });
639
- const routeModel = engine.createModel({
640
- uid: 'public-form-route-model',
641
- use: 'FlowModel',
642
- });
643
- routeModel.context.defineProperty('layout', {
808
+
809
+ const layoutModel: MockAdminLayoutModel = Object.assign(
810
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
811
+ {
812
+ registerRoutePage: vi.fn(),
813
+ updateRoutePage: vi.fn(),
814
+ unregisterRoutePage: vi.fn(),
815
+ },
816
+ );
817
+ Object.defineProperty(layoutModel, 'layout', {
644
818
  value: {
645
819
  routeName: 'public-forms',
646
820
  routePath: '/public-forms',
@@ -653,42 +827,30 @@ describe('FlowRoute', () => {
653
827
  },
654
828
  });
655
829
 
656
- const layoutModel: MockAdminLayoutModel = Object.assign(
657
- engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
658
- {
659
- registerRoutePage: vi.fn(),
660
- updateRoutePage: vi.fn(),
661
- unregisterRoutePage: vi.fn(),
662
- },
663
- );
664
-
665
830
  render(
666
831
  <FlowEngineProvider engine={engine}>
667
- <FlowContextProvider context={routeModel.context}>
668
- <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
669
- <Routes>
670
- <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
671
- </Routes>
672
- </MemoryRouter>
673
- </FlowContextProvider>
832
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
833
+ <Routes>
834
+ <Route
835
+ path="/public-forms/:name"
836
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel as any} />}
837
+ />
838
+ </Routes>
839
+ </MemoryRouter>
674
840
  </FlowEngineProvider>,
675
841
  );
676
842
 
677
843
  await waitFor(() => {
678
844
  expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
679
845
  });
680
- expect(findOne).toHaveBeenCalledWith({ uid: 'public-form-1' });
681
- expect(request).not.toHaveBeenCalled();
682
- expect(engine.getModel('public-form-1')).toBeUndefined();
846
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
683
847
  });
684
848
 
685
- it('should not skip accessible route loading just because layout authCheck is false', async () => {
849
+ it('should render not found when admin route is not accessible', async () => {
686
850
  const engine = new FlowEngine();
687
- const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
688
- const getRouteBySchemaUid = vi.fn();
689
851
  engine.setModelRepository({
690
852
  findOne: vi.fn().mockResolvedValue({
691
- uid: 'public-form-1',
853
+ uid: 'admin-denied-page',
692
854
  use: 'FlowModel',
693
855
  }),
694
856
  save: vi.fn(),
@@ -697,9 +859,9 @@ describe('FlowRoute', () => {
697
859
  engine.context.defineProperty('routeRepository', {
698
860
  value: {
699
861
  refreshAccessible: hookState.refresh,
700
- isAccessibleLoaded: () => false,
701
- ensureAccessibleLoaded,
702
- getRouteBySchemaUid,
862
+ isAccessibleLoaded: () => true,
863
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
864
+ getRouteBySchemaUid: vi.fn(() => undefined),
703
865
  },
704
866
  });
705
867
  engine.context.defineProperty('app', {
@@ -710,25 +872,9 @@ describe('FlowRoute', () => {
710
872
  },
711
873
  },
712
874
  });
713
- const routeModel = engine.createModel({
714
- uid: 'public-form-route-model',
715
- use: 'FlowModel',
716
- });
717
- routeModel.context.defineProperty('layout', {
718
- value: {
719
- routeName: 'public-forms',
720
- routePath: '/public-forms',
721
- rootRouteName: 'public-forms',
722
- uid: 'public-form-layout-model',
723
- layoutModelClass: 'PublicFormLayoutModel',
724
- rootPageModelClass: 'PublicFormPageModel',
725
- childPageModelClass: 'ChildPageModel',
726
- authCheck: false,
727
- },
728
- });
729
875
 
730
- const layoutModel: MockAdminLayoutModel = Object.assign(
731
- engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
876
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
877
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
732
878
  {
733
879
  registerRoutePage: vi.fn(),
734
880
  updateRoutePage: vi.fn(),
@@ -738,41 +884,879 @@ describe('FlowRoute', () => {
738
884
 
739
885
  render(
740
886
  <FlowEngineProvider engine={engine}>
741
- <FlowContextProvider context={routeModel.context}>
742
- <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
743
- <Routes>
744
- <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
745
- </Routes>
746
- </MemoryRouter>
747
- </FlowContextProvider>
887
+ <MemoryRouter initialEntries={['/admin/admin-denied-page']}>
888
+ <Routes>
889
+ <Route path="/admin/:name" element={<FlowRoute />} />
890
+ </Routes>
891
+ </MemoryRouter>
748
892
  </FlowEngineProvider>,
749
893
  );
750
894
 
751
- await waitFor(() => {
752
- expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
753
- });
754
- expect(ensureAccessibleLoaded).toHaveBeenCalledTimes(1);
755
- expect(getRouteBySchemaUid).not.toHaveBeenCalled();
895
+ expect(await screen.findByText('404')).toBeInTheDocument();
896
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
756
897
  });
757
898
 
758
- it('should bridge legacy page when behavior is bridge', async () => {
759
- const originalLocation = window.location;
760
- const replace = vi.fn();
761
- Object.defineProperty(window, 'location', {
762
- configurable: true,
899
+ it('should render blank content when current admin route matches an accessible empty group id', async () => {
900
+ const engine = new FlowEngine();
901
+ const listAccessible = vi.fn(() => [
902
+ {
903
+ id: 371750686228480,
904
+ schemaUid: 'group-schema',
905
+ title: '789',
906
+ type: NocoBaseDesktopRouteType.group,
907
+ },
908
+ ]);
909
+ engine.context.defineProperty('routeRepository', {
763
910
  value: {
764
- ...originalLocation,
765
- pathname: '/v2/admin/test-page',
766
- replace,
911
+ refreshAccessible: hookState.refresh,
912
+ isAccessibleLoaded: () => true,
913
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
914
+ getRouteBySchemaUid: vi.fn(() => undefined),
915
+ listAccessible,
767
916
  },
768
917
  });
769
-
770
- try {
771
- const engine = new FlowEngine();
772
- engine.context.defineProperty('routeRepository', {
773
- value: {
774
- refreshAccessible: hookState.refresh,
775
- isAccessibleLoaded: () => true,
918
+ engine.context.defineProperty('app', {
919
+ value: {
920
+ getPublicPath: () => '/v2/',
921
+ router: {
922
+ getBasename: () => '/v2',
923
+ },
924
+ },
925
+ });
926
+
927
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
928
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
929
+ {
930
+ registerRoutePage: vi.fn(),
931
+ updateRoutePage: vi.fn(),
932
+ unregisterRoutePage: vi.fn(),
933
+ },
934
+ );
935
+
936
+ const { container } = render(
937
+ <FlowEngineProvider engine={engine}>
938
+ <MemoryRouter initialEntries={['/admin/371750686228480']}>
939
+ <Routes>
940
+ <Route path="/admin/:name" element={<FlowRoute />} />
941
+ </Routes>
942
+ </MemoryRouter>
943
+ </FlowEngineProvider>,
944
+ );
945
+
946
+ await waitFor(() => {
947
+ expect(listAccessible).toHaveBeenCalled();
948
+ });
949
+ expect(container).toBeEmptyDOMElement();
950
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
951
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
952
+ });
953
+
954
+ it('should navigate a current admin group route to its first accessible flow page', async () => {
955
+ const engine = new FlowEngine();
956
+ const childRoute = {
957
+ schemaUid: 'child-flow-page',
958
+ title: 'Child flow page',
959
+ type: NocoBaseDesktopRouteType.flowPage,
960
+ };
961
+ const groupRoute = {
962
+ id: 371750686228480,
963
+ schemaUid: 'group-schema',
964
+ title: 'Group with child',
965
+ type: NocoBaseDesktopRouteType.group,
966
+ children: [childRoute],
967
+ };
968
+ const getRouteBySchemaUid = vi.fn((schemaUid: string) =>
969
+ schemaUid === 'child-flow-page' ? childRoute : undefined,
970
+ );
971
+ const listAccessible = vi.fn(() => [groupRoute]);
972
+ engine.context.defineProperty('routeRepository', {
973
+ value: {
974
+ refreshAccessible: hookState.refresh,
975
+ isAccessibleLoaded: () => true,
976
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
977
+ getRouteBySchemaUid,
978
+ listAccessible,
979
+ },
980
+ });
981
+ engine.context.defineProperty('app', {
982
+ value: {
983
+ getPublicPath: () => '/v2/',
984
+ router: {
985
+ getBasename: () => '/v2',
986
+ },
987
+ },
988
+ });
989
+
990
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
991
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
992
+ {
993
+ registerRoutePage: vi.fn(),
994
+ updateRoutePage: vi.fn(),
995
+ unregisterRoutePage: vi.fn(),
996
+ },
997
+ );
998
+
999
+ render(
1000
+ <FlowEngineProvider engine={engine}>
1001
+ <MemoryRouter initialEntries={['/admin/371750686228480']}>
1002
+ <Routes>
1003
+ <Route path="/admin/:name" element={<FlowRoute />} />
1004
+ </Routes>
1005
+ </MemoryRouter>
1006
+ </FlowEngineProvider>,
1007
+ );
1008
+
1009
+ await waitFor(() => {
1010
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page', expect.any(Object));
1011
+ });
1012
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
1013
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1014
+ });
1015
+
1016
+ it('should keep rendering 404 when a group id is opened with a nested admin route path', async () => {
1017
+ const engine = new FlowEngine();
1018
+ const childRoute = {
1019
+ schemaUid: 'child-flow-page',
1020
+ title: 'Child flow page',
1021
+ type: NocoBaseDesktopRouteType.flowPage,
1022
+ };
1023
+ const groupRoute = {
1024
+ id: 371750686228480,
1025
+ schemaUid: 'group-schema',
1026
+ title: 'Group with child',
1027
+ type: NocoBaseDesktopRouteType.group,
1028
+ children: [childRoute],
1029
+ };
1030
+ engine.context.defineProperty('routeRepository', {
1031
+ value: {
1032
+ refreshAccessible: hookState.refresh,
1033
+ isAccessibleLoaded: () => true,
1034
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1035
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
1036
+ listAccessible: vi.fn(() => [groupRoute]),
1037
+ },
1038
+ });
1039
+ engine.context.defineProperty('app', {
1040
+ value: {
1041
+ getPublicPath: () => '/v2/',
1042
+ router: {
1043
+ getBasename: () => '/v2',
1044
+ },
1045
+ },
1046
+ });
1047
+
1048
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1049
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1050
+ {
1051
+ registerRoutePage: vi.fn(),
1052
+ updateRoutePage: vi.fn(),
1053
+ unregisterRoutePage: vi.fn(),
1054
+ },
1055
+ );
1056
+
1057
+ render(
1058
+ <FlowEngineProvider engine={engine}>
1059
+ <MemoryRouter initialEntries={['/admin/371750686228480/view/not-exists']}>
1060
+ <Routes>
1061
+ <Route path="/admin/:name/view/*" element={<FlowRoute />} />
1062
+ </Routes>
1063
+ </MemoryRouter>
1064
+ </FlowEngineProvider>,
1065
+ );
1066
+
1067
+ expect(await screen.findByText('404')).toBeInTheDocument();
1068
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('child-flow-page', expect.any(Object));
1069
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
1070
+ });
1071
+
1072
+ it('should keep rendering 404 when a group id is opened with a tab admin route path', async () => {
1073
+ const engine = new FlowEngine();
1074
+ const childRoute = {
1075
+ schemaUid: 'child-flow-page',
1076
+ title: 'Child flow page',
1077
+ type: NocoBaseDesktopRouteType.flowPage,
1078
+ };
1079
+ const groupRoute = {
1080
+ id: 371750686228480,
1081
+ schemaUid: 'group-schema',
1082
+ title: 'Group with child',
1083
+ type: NocoBaseDesktopRouteType.group,
1084
+ children: [childRoute],
1085
+ };
1086
+ engine.context.defineProperty('routeRepository', {
1087
+ value: {
1088
+ refreshAccessible: hookState.refresh,
1089
+ isAccessibleLoaded: () => true,
1090
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1091
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
1092
+ listAccessible: vi.fn(() => [groupRoute]),
1093
+ },
1094
+ });
1095
+ engine.context.defineProperty('app', {
1096
+ value: {
1097
+ getPublicPath: () => '/v2/',
1098
+ router: {
1099
+ getBasename: () => '/v2',
1100
+ },
1101
+ },
1102
+ });
1103
+
1104
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1105
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1106
+ {
1107
+ registerRoutePage: vi.fn(),
1108
+ updateRoutePage: vi.fn(),
1109
+ unregisterRoutePage: vi.fn(),
1110
+ },
1111
+ );
1112
+
1113
+ render(
1114
+ <FlowEngineProvider engine={engine}>
1115
+ <MemoryRouter initialEntries={['/admin/371750686228480/tab/not-exists']}>
1116
+ <Routes>
1117
+ <Route path="/admin/:name/tab/:tabUid" element={<FlowRoute />} />
1118
+ </Routes>
1119
+ </MemoryRouter>
1120
+ </FlowEngineProvider>,
1121
+ );
1122
+
1123
+ expect(await screen.findByText('404')).toBeInTheDocument();
1124
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('child-flow-page', expect.any(Object));
1125
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750686228480', expect.any(Object));
1126
+ });
1127
+
1128
+ it('should keep blank content while a group child navigation is already pending for the current path', async () => {
1129
+ const engine = new FlowEngine();
1130
+ const childRoute = {
1131
+ schemaUid: 'child-flow-page',
1132
+ title: 'Child flow page',
1133
+ type: NocoBaseDesktopRouteType.flowPage,
1134
+ };
1135
+ const groupRoute = {
1136
+ id: 371750686228480,
1137
+ schemaUid: 'group-schema',
1138
+ title: 'Group with child',
1139
+ type: NocoBaseDesktopRouteType.group,
1140
+ children: [childRoute],
1141
+ };
1142
+ engine.context.defineProperty('routeRepository', {
1143
+ value: {
1144
+ refreshAccessible: hookState.refresh,
1145
+ isAccessibleLoaded: () => true,
1146
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1147
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'child-flow-page' ? childRoute : undefined)),
1148
+ listAccessible: vi.fn(() => [groupRoute]),
1149
+ },
1150
+ });
1151
+ engine.context.defineProperty('app', {
1152
+ value: {
1153
+ getPublicPath: () => '/v2/',
1154
+ router: {
1155
+ getBasename: () => '/v2',
1156
+ },
1157
+ },
1158
+ });
1159
+
1160
+ const replace = vi.fn();
1161
+ const navigator: RouterNavigator = {
1162
+ createHref: (to: { pathname?: string; search?: string; hash?: string } | string) =>
1163
+ typeof to === 'string' ? to : `${to.pathname || ''}${to.search || ''}${to.hash || ''}`,
1164
+ go: vi.fn(),
1165
+ push: vi.fn(),
1166
+ replace,
1167
+ };
1168
+
1169
+ const RerenderAfterReplace = () => {
1170
+ const [rerendered, setRerendered] = React.useState(false);
1171
+ React.useEffect(() => {
1172
+ if (!rerendered && replace.mock.calls.length > 0) {
1173
+ setRerendered(true);
1174
+ }
1175
+ }, [rerendered]);
1176
+
1177
+ return <FlowRoute legacyPageBehavior={rerendered ? 'bridge' : undefined} />;
1178
+ };
1179
+
1180
+ render(
1181
+ <FlowEngineProvider engine={engine}>
1182
+ <Router location="/admin/371750686228480" navigator={navigator}>
1183
+ <Routes>
1184
+ <Route path="/admin/:name" element={<RerenderAfterReplace />} />
1185
+ </Routes>
1186
+ </Router>
1187
+ </FlowEngineProvider>,
1188
+ );
1189
+
1190
+ await waitFor(() => {
1191
+ expect(replace).toHaveBeenCalled();
1192
+ });
1193
+ await act(async () => {
1194
+ await new Promise((resolve) => setTimeout(resolve, 0));
1195
+ });
1196
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1197
+ });
1198
+
1199
+ it('should reset group navigation guard when page uid changes in the same route instance', async () => {
1200
+ const engine = new FlowEngine();
1201
+ const childRoute1 = {
1202
+ schemaUid: 'child-flow-page-1',
1203
+ title: 'Child flow page 1',
1204
+ type: NocoBaseDesktopRouteType.flowPage,
1205
+ };
1206
+ const childRoute2 = {
1207
+ schemaUid: 'child-flow-page-2',
1208
+ title: 'Child flow page 2',
1209
+ type: NocoBaseDesktopRouteType.flowPage,
1210
+ };
1211
+ const groupRoute1 = {
1212
+ id: 371750686228480,
1213
+ schemaUid: 'group-schema-1',
1214
+ title: 'Group with child 1',
1215
+ type: NocoBaseDesktopRouteType.group,
1216
+ children: [childRoute1],
1217
+ };
1218
+ const groupRoute2 = {
1219
+ id: 371750755434496,
1220
+ schemaUid: 'group-schema-2',
1221
+ title: 'Group with child 2',
1222
+ type: NocoBaseDesktopRouteType.group,
1223
+ children: [childRoute2],
1224
+ };
1225
+ const routeMap: Record<string, typeof childRoute1 | typeof childRoute2> = {
1226
+ 'child-flow-page-1': childRoute1,
1227
+ 'child-flow-page-2': childRoute2,
1228
+ };
1229
+ const getRouteBySchemaUid = vi.fn((schemaUid: string) => routeMap[schemaUid]);
1230
+ const listAccessible = vi.fn(() => [groupRoute1, groupRoute2]);
1231
+ engine.context.defineProperty('routeRepository', {
1232
+ value: {
1233
+ refreshAccessible: hookState.refresh,
1234
+ isAccessibleLoaded: () => true,
1235
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1236
+ getRouteBySchemaUid,
1237
+ listAccessible,
1238
+ },
1239
+ });
1240
+ engine.context.defineProperty('app', {
1241
+ value: {
1242
+ getPublicPath: () => '/v2/',
1243
+ router: {
1244
+ getBasename: () => '/v2',
1245
+ },
1246
+ },
1247
+ });
1248
+
1249
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1250
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1251
+ {
1252
+ registerRoutePage: vi.fn(),
1253
+ updateRoutePage: vi.fn(),
1254
+ unregisterRoutePage: vi.fn(),
1255
+ },
1256
+ );
1257
+ let navigateTo: (path: string) => void = () => {};
1258
+ const CaptureNavigate = () => {
1259
+ const navigate = useNavigate();
1260
+ React.useEffect(() => {
1261
+ navigateTo = navigate;
1262
+ }, [navigate]);
1263
+ return null;
1264
+ };
1265
+
1266
+ render(
1267
+ <FlowEngineProvider engine={engine}>
1268
+ <MemoryRouter initialEntries={['/admin/371750686228480']}>
1269
+ <CaptureNavigate />
1270
+ <Routes>
1271
+ <Route path="/admin/:name" element={<FlowRoute />} />
1272
+ </Routes>
1273
+ </MemoryRouter>
1274
+ </FlowEngineProvider>,
1275
+ );
1276
+
1277
+ await waitFor(() => {
1278
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page-1', expect.any(Object));
1279
+ });
1280
+
1281
+ act(() => {
1282
+ navigateTo('/admin/371750755434496');
1283
+ });
1284
+
1285
+ await waitFor(() => {
1286
+ expect(adminLayoutModel.registerRoutePage).toHaveBeenCalledWith('child-flow-page-2', expect.any(Object));
1287
+ });
1288
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalledWith('371750755434496', expect.any(Object));
1289
+ expect(screen.queryByText('404')).not.toBeInTheDocument();
1290
+ });
1291
+
1292
+ it('should keep rendering 404 when current admin route matches no accessible schema uid or id', async () => {
1293
+ const engine = new FlowEngine();
1294
+ engine.context.defineProperty('routeRepository', {
1295
+ value: {
1296
+ refreshAccessible: hookState.refresh,
1297
+ isAccessibleLoaded: () => true,
1298
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1299
+ getRouteBySchemaUid: vi.fn(() => undefined),
1300
+ listAccessible: vi.fn(() => [
1301
+ {
1302
+ id: 371750686228480,
1303
+ schemaUid: 'group-schema',
1304
+ title: '789',
1305
+ type: NocoBaseDesktopRouteType.group,
1306
+ },
1307
+ ]),
1308
+ },
1309
+ });
1310
+ engine.context.defineProperty('app', {
1311
+ value: {
1312
+ getPublicPath: () => '/v2/',
1313
+ router: {
1314
+ getBasename: () => '/v2',
1315
+ },
1316
+ },
1317
+ });
1318
+
1319
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1320
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1321
+ {
1322
+ registerRoutePage: vi.fn(),
1323
+ updateRoutePage: vi.fn(),
1324
+ unregisterRoutePage: vi.fn(),
1325
+ },
1326
+ );
1327
+
1328
+ render(
1329
+ <FlowEngineProvider engine={engine}>
1330
+ <MemoryRouter initialEntries={['/admin/not-exists-4822']}>
1331
+ <Routes>
1332
+ <Route path="/admin/:name" element={<FlowRoute />} />
1333
+ </Routes>
1334
+ </MemoryRouter>
1335
+ </FlowEngineProvider>,
1336
+ );
1337
+
1338
+ expect(await screen.findByText('404')).toBeInTheDocument();
1339
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1340
+ });
1341
+
1342
+ it('should keep rendering 404 when current admin route matches only a non-group route id', async () => {
1343
+ const engine = new FlowEngine();
1344
+ engine.context.defineProperty('routeRepository', {
1345
+ value: {
1346
+ refreshAccessible: hookState.refresh,
1347
+ isAccessibleLoaded: () => true,
1348
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1349
+ getRouteBySchemaUid: vi.fn(() => undefined),
1350
+ listAccessible: vi.fn(() => [
1351
+ {
1352
+ id: 10001,
1353
+ schemaUid: 'flow-page-schema',
1354
+ title: 'Flow page',
1355
+ type: NocoBaseDesktopRouteType.flowPage,
1356
+ },
1357
+ ]),
1358
+ },
1359
+ });
1360
+ engine.context.defineProperty('app', {
1361
+ value: {
1362
+ getPublicPath: () => '/v2/',
1363
+ router: {
1364
+ getBasename: () => '/v2',
1365
+ },
1366
+ },
1367
+ });
1368
+
1369
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1370
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1371
+ {
1372
+ registerRoutePage: vi.fn(),
1373
+ updateRoutePage: vi.fn(),
1374
+ unregisterRoutePage: vi.fn(),
1375
+ },
1376
+ );
1377
+
1378
+ render(
1379
+ <FlowEngineProvider engine={engine}>
1380
+ <MemoryRouter initialEntries={['/admin/10001']}>
1381
+ <Routes>
1382
+ <Route path="/admin/:name" element={<FlowRoute />} />
1383
+ </Routes>
1384
+ </MemoryRouter>
1385
+ </FlowEngineProvider>,
1386
+ );
1387
+
1388
+ expect(await screen.findByText('404')).toBeInTheDocument();
1389
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1390
+ });
1391
+
1392
+ it('should keep rendering 404 when current admin route matches only a group schema uid', async () => {
1393
+ const engine = new FlowEngine();
1394
+ const groupRoute = {
1395
+ id: 371750686228480,
1396
+ schemaUid: 'group-schema',
1397
+ title: '789',
1398
+ type: NocoBaseDesktopRouteType.group,
1399
+ };
1400
+ engine.context.defineProperty('routeRepository', {
1401
+ value: {
1402
+ refreshAccessible: hookState.refresh,
1403
+ isAccessibleLoaded: () => true,
1404
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1405
+ getRouteBySchemaUid: vi.fn((schemaUid: string) => (schemaUid === 'group-schema' ? groupRoute : undefined)),
1406
+ listAccessible: vi.fn(() => [groupRoute]),
1407
+ },
1408
+ });
1409
+ engine.context.defineProperty('app', {
1410
+ value: {
1411
+ getPublicPath: () => '/v2/',
1412
+ router: {
1413
+ getBasename: () => '/v2',
1414
+ },
1415
+ },
1416
+ });
1417
+
1418
+ const adminLayoutModel: MockAdminLayoutModel = Object.assign(
1419
+ engine.createModel({ uid: 'admin-layout-model', use: 'FlowModel' }),
1420
+ {
1421
+ registerRoutePage: vi.fn(),
1422
+ updateRoutePage: vi.fn(),
1423
+ unregisterRoutePage: vi.fn(),
1424
+ },
1425
+ );
1426
+
1427
+ render(
1428
+ <FlowEngineProvider engine={engine}>
1429
+ <MemoryRouter initialEntries={['/admin/group-schema']}>
1430
+ <Routes>
1431
+ <Route path="/admin/:name" element={<FlowRoute />} />
1432
+ </Routes>
1433
+ </MemoryRouter>
1434
+ </FlowEngineProvider>,
1435
+ );
1436
+
1437
+ expect(await screen.findByText('404')).toBeInTheDocument();
1438
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
1439
+ });
1440
+
1441
+ it('should not use flowModels:findOne to bridge a missing route for protected layouts', async () => {
1442
+ const engine = new FlowEngine();
1443
+ const findOne = vi.fn().mockResolvedValue({
1444
+ uid: 'mobile-denied-page',
1445
+ use: 'FlowModel',
1446
+ });
1447
+ engine.setModelRepository({
1448
+ findOne,
1449
+ save: vi.fn(),
1450
+ destroy: vi.fn(),
1451
+ } as any);
1452
+ engine.context.defineProperty('routeRepository', {
1453
+ value: {
1454
+ refreshAccessible: hookState.refresh,
1455
+ isAccessibleLoaded: () => true,
1456
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1457
+ getRouteBySchemaUid: vi.fn(() => undefined),
1458
+ },
1459
+ });
1460
+ engine.context.defineProperty('app', {
1461
+ value: {
1462
+ getPublicPath: () => '/v2/',
1463
+ router: {
1464
+ getBasename: () => '/v2',
1465
+ },
1466
+ },
1467
+ });
1468
+ const routeModel = engine.createModel({
1469
+ uid: 'mobile-route-model',
1470
+ use: 'FlowModel',
1471
+ });
1472
+ routeModel.context.defineProperty('layout', {
1473
+ value: {
1474
+ routeName: 'mobile',
1475
+ routePath: '/mobile',
1476
+ rootRouteName: 'mobile',
1477
+ uid: 'mobile-layout-model',
1478
+ layoutModelClass: 'MobileLayoutModel',
1479
+ rootPageModelClass: 'MobileRootPageModel',
1480
+ childPageModelClass: 'MobileChildPageModel',
1481
+ authCheck: true,
1482
+ },
1483
+ });
1484
+
1485
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1486
+ engine.createModel({ uid: 'mobile-layout-model', use: 'FlowModel' }),
1487
+ {
1488
+ registerRoutePage: vi.fn(),
1489
+ updateRoutePage: vi.fn(),
1490
+ unregisterRoutePage: vi.fn(),
1491
+ },
1492
+ );
1493
+
1494
+ render(
1495
+ <FlowEngineProvider engine={engine}>
1496
+ <FlowContextProvider context={routeModel.context}>
1497
+ <MemoryRouter initialEntries={['/mobile/mobile-denied-page']}>
1498
+ <Routes>
1499
+ <Route
1500
+ path="/mobile/:name"
1501
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
1502
+ />
1503
+ </Routes>
1504
+ </MemoryRouter>
1505
+ </FlowContextProvider>
1506
+ </FlowEngineProvider>,
1507
+ );
1508
+
1509
+ expect(await screen.findByText('404')).toBeInTheDocument();
1510
+ expect(findOne).not.toHaveBeenCalled();
1511
+ expect(layoutModel.registerRoutePage).not.toHaveBeenCalled();
1512
+ });
1513
+
1514
+ it('should not load a known page uid when a protected custom layout has no accessible route', async () => {
1515
+ const engine = new FlowEngine();
1516
+ const findOne = vi.fn().mockResolvedValue({
1517
+ uid: 'known-denied-custom-page',
1518
+ use: 'FlowModel',
1519
+ });
1520
+ engine.setModelRepository({
1521
+ findOne,
1522
+ save: vi.fn(),
1523
+ destroy: vi.fn(),
1524
+ } as any);
1525
+ engine.context.defineProperty('routeRepository', {
1526
+ value: {
1527
+ refreshAccessible: hookState.refresh,
1528
+ isAccessibleLoaded: () => true,
1529
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1530
+ getRouteBySchemaUid: vi.fn(() => undefined),
1531
+ },
1532
+ });
1533
+ engine.context.defineProperty('app', {
1534
+ value: {
1535
+ getPublicPath: () => '/v2/',
1536
+ router: {
1537
+ getBasename: () => '/v2',
1538
+ },
1539
+ },
1540
+ });
1541
+ const routeModel = engine.createModel({
1542
+ uid: 'custom-layout-route-model',
1543
+ use: 'FlowModel',
1544
+ });
1545
+ routeModel.context.defineProperty('layout', {
1546
+ value: {
1547
+ routeName: 'admin2',
1548
+ routePath: '/admin2',
1549
+ rootRouteName: 'admin2',
1550
+ uid: 'custom-protected-layout-model',
1551
+ layoutModelClass: 'AdminLayoutModel',
1552
+ rootPageModelClass: 'RootPageModel',
1553
+ childPageModelClass: 'ChildPageModel',
1554
+ authCheck: true,
1555
+ },
1556
+ });
1557
+
1558
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1559
+ engine.createModel({ uid: 'custom-protected-layout-model', use: 'FlowModel' }),
1560
+ {
1561
+ registerRoutePage: vi.fn(),
1562
+ updateRoutePage: vi.fn(),
1563
+ unregisterRoutePage: vi.fn(),
1564
+ },
1565
+ );
1566
+
1567
+ render(
1568
+ <FlowEngineProvider engine={engine}>
1569
+ <FlowContextProvider context={routeModel.context}>
1570
+ <MemoryRouter initialEntries={['/admin2/known-denied-custom-page']}>
1571
+ <Routes>
1572
+ <Route
1573
+ path="/admin2/:name"
1574
+ element={<FlowRoute legacyPageBehavior="notFound" getLayoutModel={() => layoutModel} />}
1575
+ />
1576
+ </Routes>
1577
+ </MemoryRouter>
1578
+ </FlowContextProvider>
1579
+ </FlowEngineProvider>,
1580
+ );
1581
+
1582
+ expect(await screen.findByText('404')).toBeInTheDocument();
1583
+ expect(findOne).not.toHaveBeenCalled();
1584
+ expect(layoutModel.registerRoutePage).not.toHaveBeenCalled();
1585
+ });
1586
+
1587
+ it('should check model existence without occupying the route model uid', async () => {
1588
+ const engine = new FlowEngine();
1589
+ const findOne = vi.fn().mockResolvedValue({
1590
+ uid: 'public-form-1',
1591
+ });
1592
+ const request = vi.fn().mockResolvedValue({
1593
+ data: {
1594
+ data: {
1595
+ uid: 'public-form-1',
1596
+ },
1597
+ },
1598
+ });
1599
+ engine.setModelRepository({
1600
+ findOne,
1601
+ save: vi.fn(),
1602
+ destroy: vi.fn(),
1603
+ } as any);
1604
+ engine.context.defineProperty('routeRepository', {
1605
+ value: {
1606
+ refreshAccessible: hookState.refresh,
1607
+ isAccessibleLoaded: () => true,
1608
+ ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
1609
+ getRouteBySchemaUid: vi.fn(() => undefined),
1610
+ },
1611
+ });
1612
+ engine.context.defineProperty('app', {
1613
+ value: {
1614
+ apiClient: {
1615
+ request,
1616
+ },
1617
+ getPublicPath: () => '/v2/',
1618
+ router: {
1619
+ getBasename: () => '/v2',
1620
+ },
1621
+ },
1622
+ });
1623
+ const routeModel = engine.createModel({
1624
+ uid: 'public-form-route-model',
1625
+ use: 'FlowModel',
1626
+ });
1627
+ routeModel.context.defineProperty('layout', {
1628
+ value: {
1629
+ routeName: 'public-forms',
1630
+ routePath: '/public-forms',
1631
+ rootRouteName: 'public-forms',
1632
+ uid: 'public-form-layout-model',
1633
+ layoutModelClass: 'PublicFormLayoutModel',
1634
+ rootPageModelClass: 'PublicFormPageModel',
1635
+ childPageModelClass: 'ChildPageModel',
1636
+ authCheck: false,
1637
+ },
1638
+ });
1639
+
1640
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1641
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
1642
+ {
1643
+ registerRoutePage: vi.fn(),
1644
+ updateRoutePage: vi.fn(),
1645
+ unregisterRoutePage: vi.fn(),
1646
+ },
1647
+ );
1648
+
1649
+ render(
1650
+ <FlowEngineProvider engine={engine}>
1651
+ <FlowContextProvider context={routeModel.context}>
1652
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
1653
+ <Routes>
1654
+ <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
1655
+ </Routes>
1656
+ </MemoryRouter>
1657
+ </FlowContextProvider>
1658
+ </FlowEngineProvider>,
1659
+ );
1660
+
1661
+ await waitFor(() => {
1662
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
1663
+ });
1664
+ expect(findOne).toHaveBeenCalledWith({ uid: 'public-form-1' });
1665
+ expect(request).not.toHaveBeenCalled();
1666
+ expect(engine.getModel('public-form-1')).toBeUndefined();
1667
+ });
1668
+
1669
+ it('should skip accessible route loading when layout authCheck is false', async () => {
1670
+ const engine = new FlowEngine();
1671
+ const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
1672
+ const getRouteBySchemaUid = vi.fn();
1673
+ engine.setModelRepository({
1674
+ findOne: vi.fn().mockResolvedValue({
1675
+ uid: 'public-form-1',
1676
+ use: 'FlowModel',
1677
+ }),
1678
+ save: vi.fn(),
1679
+ destroy: vi.fn(),
1680
+ } as any);
1681
+ engine.context.defineProperty('routeRepository', {
1682
+ value: {
1683
+ refreshAccessible: hookState.refresh,
1684
+ isAccessibleLoaded: () => false,
1685
+ ensureAccessibleLoaded,
1686
+ getRouteBySchemaUid,
1687
+ },
1688
+ });
1689
+ engine.context.defineProperty('app', {
1690
+ value: {
1691
+ getPublicPath: () => '/v2/',
1692
+ router: {
1693
+ getBasename: () => '/v2',
1694
+ },
1695
+ },
1696
+ });
1697
+ const routeModel = engine.createModel({
1698
+ uid: 'public-form-route-model',
1699
+ use: 'FlowModel',
1700
+ });
1701
+ routeModel.context.defineProperty('layout', {
1702
+ value: {
1703
+ routeName: 'public-forms',
1704
+ routePath: '/public-forms',
1705
+ rootRouteName: 'public-forms',
1706
+ uid: 'public-form-layout-model',
1707
+ layoutModelClass: 'PublicFormLayoutModel',
1708
+ rootPageModelClass: 'PublicFormPageModel',
1709
+ childPageModelClass: 'ChildPageModel',
1710
+ authCheck: false,
1711
+ },
1712
+ });
1713
+
1714
+ const layoutModel: MockAdminLayoutModel = Object.assign(
1715
+ engine.createModel({ uid: 'public-form-layout-model', use: 'FlowModel' }),
1716
+ {
1717
+ registerRoutePage: vi.fn(),
1718
+ updateRoutePage: vi.fn(),
1719
+ unregisterRoutePage: vi.fn(),
1720
+ },
1721
+ );
1722
+
1723
+ render(
1724
+ <FlowEngineProvider engine={engine}>
1725
+ <FlowContextProvider context={routeModel.context}>
1726
+ <MemoryRouter initialEntries={['/public-forms/public-form-1']}>
1727
+ <Routes>
1728
+ <Route path="/public-forms/:name" element={<FlowRoute legacyPageBehavior="notFound" />} />
1729
+ </Routes>
1730
+ </MemoryRouter>
1731
+ </FlowContextProvider>
1732
+ </FlowEngineProvider>,
1733
+ );
1734
+
1735
+ await waitFor(() => {
1736
+ expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
1737
+ });
1738
+ expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
1739
+ expect(getRouteBySchemaUid).not.toHaveBeenCalled();
1740
+ });
1741
+
1742
+ it('should bridge legacy page when behavior is bridge', async () => {
1743
+ const originalLocation = window.location;
1744
+ const replace = vi.fn();
1745
+ Object.defineProperty(window, 'location', {
1746
+ configurable: true,
1747
+ value: {
1748
+ ...originalLocation,
1749
+ pathname: '/v2/admin/test-page',
1750
+ replace,
1751
+ },
1752
+ });
1753
+
1754
+ try {
1755
+ const engine = new FlowEngine();
1756
+ engine.context.defineProperty('routeRepository', {
1757
+ value: {
1758
+ refreshAccessible: hookState.refresh,
1759
+ isAccessibleLoaded: () => true,
776
1760
  ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
777
1761
  getRouteBySchemaUid: vi.fn(() => ({ type: 'page', schemaUid: 'test-page' })),
778
1762
  },
@@ -817,7 +1801,7 @@ describe('FlowRoute', () => {
817
1801
  }
818
1802
  });
819
1803
 
820
- it('should not redirect when route does not exist', async () => {
1804
+ it('should render not found without redirecting when admin route does not exist', async () => {
821
1805
  const originalLocation = window.location;
822
1806
  const replace = vi.fn();
823
1807
  Object.defineProperty(window, 'location', {
@@ -866,9 +1850,8 @@ describe('FlowRoute', () => {
866
1850
  </FlowEngineProvider>,
867
1851
  );
868
1852
 
869
- await waitFor(() => {
870
- expect(adminLayoutModel.registerRoutePage).toHaveBeenCalled();
871
- });
1853
+ expect(await screen.findByText('404')).toBeInTheDocument();
1854
+ expect(adminLayoutModel.registerRoutePage).not.toHaveBeenCalled();
872
1855
  expect(replace).not.toHaveBeenCalled();
873
1856
  } finally {
874
1857
  Object.defineProperty(window, 'location', {