@antelopejs/dms 0.8.3 → 0.9.0

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 (351) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/implementations/dms/frontend-backend-proxies.d.ts +3 -0
  3. package/dist/implementations/dms/frontend-backend-proxies.js +39 -0
  4. package/dist/implementations/dms/frontend-backend-proxies.js.map +1 -0
  5. package/dist/implementations/dms/frontend-bootstrap.d.ts +1 -0
  6. package/dist/implementations/dms/frontend-bootstrap.js +13 -0
  7. package/dist/implementations/dms/frontend-bootstrap.js.map +1 -1
  8. package/dist/implementations/dms/frontend-modules.d.ts +4 -2
  9. package/dist/implementations/dms/frontend-modules.js +8 -7
  10. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  11. package/dist/implementations/dms/manifest.d.ts +6 -0
  12. package/dist/implementations/dms/manifest.js +13 -0
  13. package/dist/implementations/dms/manifest.js.map +1 -1
  14. package/dist/implementations/dms/menu-categories.d.ts +16 -0
  15. package/dist/implementations/dms/menu-categories.js +30 -0
  16. package/dist/implementations/dms/menu-categories.js.map +1 -0
  17. package/dist/implementations/dms/nav-badges.d.ts +0 -8
  18. package/dist/implementations/dms/nav-badges.js +0 -16
  19. package/dist/implementations/dms/nav-badges.js.map +1 -1
  20. package/dist/implementations/dms/page.d.ts +11 -5
  21. package/dist/implementations/dms/page.js +56 -41
  22. package/dist/implementations/dms/page.js.map +1 -1
  23. package/dist/implementations/dms-html-render/index.js +5 -2
  24. package/dist/implementations/dms-html-render/index.js.map +1 -1
  25. package/dist/implementations/dms-notifications/index.d.ts +4 -3
  26. package/dist/implementations/dms-notifications/index.js +14 -6
  27. package/dist/implementations/dms-notifications/index.js.map +1 -1
  28. package/dist/pages/settings/settings-index.js +3 -2
  29. package/dist/pages/settings/settings-index.js.map +1 -1
  30. package/dist/pages/settings/users/invites.js +2 -2
  31. package/dist/pages/settings/users/invites.js.map +1 -1
  32. package/dist/pages/settings/users/member-change-notifications.js +17 -6
  33. package/dist/pages/settings/users/member-change-notifications.js.map +1 -1
  34. package/dist/pages/settings/users/member-invite-form.d.ts +3 -2
  35. package/dist/pages/settings/users/member-invite-form.js +7 -4
  36. package/dist/pages/settings/users/member-invite-form.js.map +1 -1
  37. package/dist/pages/settings/users/member-roles-field.js +2 -1
  38. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  39. package/dist/pages/settings/users/members.js +4 -1
  40. package/dist/pages/settings/users/members.js.map +1 -1
  41. package/dist/pages/settings/users/notifications.d.ts +6 -0
  42. package/dist/pages/settings/users/notifications.js +23 -15
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/profile.js +1 -1
  45. package/dist/pages/settings/users/profile.js.map +1 -1
  46. package/dist/pages/settings/users/region-form.d.ts +1 -1
  47. package/dist/pages/settings/users/region-form.js +2 -1
  48. package/dist/pages/settings/users/region-form.js.map +1 -1
  49. package/dist/pages/settings/users/role-editor-store.d.ts +3 -1
  50. package/dist/pages/settings/users/role-editor-store.js +17 -4
  51. package/dist/pages/settings/users/role-editor-store.js.map +1 -1
  52. package/dist/pages/settings/users/roles.js +1 -0
  53. package/dist/pages/settings/users/roles.js.map +1 -1
  54. package/dist/realtime/registry.d.ts +1 -0
  55. package/dist/realtime/registry.js +5 -0
  56. package/dist/realtime/registry.js.map +1 -1
  57. package/dist/routes/realtime.d.ts +5 -1
  58. package/dist/routes/realtime.js +16 -9
  59. package/dist/routes/realtime.js.map +1 -1
  60. package/dist/test/integration/account-deletion.test.js +2 -2
  61. package/dist/test/integration/account-deletion.test.js.map +1 -1
  62. package/dist/test/integration/auth/auth-key-rotation.test.js +2 -1
  63. package/dist/test/integration/auth/auth-key-rotation.test.js.map +1 -1
  64. package/dist/test/integration/auth/current-password.test.js +2 -1
  65. package/dist/test/integration/auth/current-password.test.js.map +1 -1
  66. package/dist/test/integration/auth/session-revocation.test.js +2 -1
  67. package/dist/test/integration/auth/session-revocation.test.js.map +1 -1
  68. package/dist/test/integration/auth/sign-in-country.test.js +2 -1
  69. package/dist/test/integration/auth/sign-in-country.test.js.map +1 -1
  70. package/dist/test/integration/auth/two-factor-codes.test.js +2 -1
  71. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  72. package/dist/test/integration/auth/two-factor-password.test.js +3 -2
  73. package/dist/test/integration/auth/two-factor-password.test.js.map +1 -1
  74. package/dist/test/integration/email-change.test.js +2 -1
  75. package/dist/test/integration/email-change.test.js.map +1 -1
  76. package/dist/test/integration/files/component-files.test.js +2 -2
  77. package/dist/test/integration/files/component-files.test.js.map +1 -1
  78. package/dist/test/integration/member-settings-access.test.js +8 -7
  79. package/dist/test/integration/member-settings-access.test.js.map +1 -1
  80. package/dist/test/integration/menu-access-events.test.d.ts +1 -0
  81. package/dist/test/integration/menu-access-events.test.js +148 -0
  82. package/dist/test/integration/menu-access-events.test.js.map +1 -0
  83. package/dist/test/integration/notification-inbox.test.js +4 -1
  84. package/dist/test/integration/notification-inbox.test.js.map +1 -1
  85. package/dist/test/integration/notification-locked-subjects.test.js +3 -2
  86. package/dist/test/integration/notification-locked-subjects.test.js.map +1 -1
  87. package/dist/test/integration/notification-unread-badge.test.js +51 -5
  88. package/dist/test/integration/notification-unread-badge.test.js.map +1 -1
  89. package/dist/test/integration/page-layout-route-guard.test.d.ts +1 -0
  90. package/dist/test/integration/page-layout-route-guard.test.js +56 -0
  91. package/dist/test/integration/page-layout-route-guard.test.js.map +1 -0
  92. package/dist/test/integration/permission-warnings.test.js +3 -2
  93. package/dist/test/integration/permission-warnings.test.js.map +1 -1
  94. package/dist/test/integration/removed-member-notifications.test.js +3 -2
  95. package/dist/test/integration/removed-member-notifications.test.js.map +1 -1
  96. package/dist/test/integration/role-delete-reassign.test.js +2 -1
  97. package/dist/test/integration/role-delete-reassign.test.js.map +1 -1
  98. package/dist/test/integration/role-permission-ancestors.test.js +2 -1
  99. package/dist/test/integration/role-permission-ancestors.test.js.map +1 -1
  100. package/dist/test/unit/frontend-bootstrap.test.js +13 -0
  101. package/dist/test/unit/frontend-bootstrap.test.js.map +1 -1
  102. package/dist/test/unit/implementations/dms/category-slug-registry.test.js +14 -5
  103. package/dist/test/unit/implementations/dms/category-slug-registry.test.js.map +1 -1
  104. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.d.ts +1 -0
  105. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js +66 -0
  106. package/dist/test/unit/implementations/dms/frontend-backend-proxies.test.js.map +1 -0
  107. package/dist/test/unit/implementations/dms/manifest-strip.test.js +26 -0
  108. package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
  109. package/dist/test/unit/implementations/dms/nav-badges.test.js +23 -9
  110. package/dist/test/unit/implementations/dms/nav-badges.test.js.map +1 -1
  111. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +22 -4
  112. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  113. package/dist/test/unit/interfaces/dms/page-layout-query.test.js +12 -0
  114. package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -1
  115. package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -1
  116. package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
  117. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  118. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  120. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  121. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  122. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  123. package/frontend-vue/deferred-plugins.ts +3 -4
  124. package/frontend-vue/dms.email.ts +5 -1
  125. package/frontend-vue/dms.frontend.ts +15 -5
  126. package/frontend-vue/dms.server.ts +74 -0
  127. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  128. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  129. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  130. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  131. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  136. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  137. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  138. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  139. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  140. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  141. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  142. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  143. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  144. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  145. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  146. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  148. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  150. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  151. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  152. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  153. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  154. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  155. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  156. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  157. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  158. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  159. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  160. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  161. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  162. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  163. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  164. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  165. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  166. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  167. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  168. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  169. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  170. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  171. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  172. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  179. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  180. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  181. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  182. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  183. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  184. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  185. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  186. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  187. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  191. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  192. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  193. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  194. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  195. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  196. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  197. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  198. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  199. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  200. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  201. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  202. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  203. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  204. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  205. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  206. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  207. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  208. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  209. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  210. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  211. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  212. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  213. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  214. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  215. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  216. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  217. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  218. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  219. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  220. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  221. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  222. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  223. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  224. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  225. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  226. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  227. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  228. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  230. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  231. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  232. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  233. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  234. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  235. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  238. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  239. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  240. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  241. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  244. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  245. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  246. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  247. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  248. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  249. package/frontend-vue/package.json +2 -2
  250. package/frontend-vue/pnpm-lock.yaml +5 -5
  251. package/frontend-vue/server/private-options.ts +27 -0
  252. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  253. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  254. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  255. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  256. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  257. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  258. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  259. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  260. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  261. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  262. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  263. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  264. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  265. package/frontend-vue/tests/email-layout.test.ts +66 -4
  266. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  267. package/frontend-vue/tests/favicon.test.ts +131 -0
  268. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  269. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  270. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  271. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  272. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  273. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  274. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  275. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  276. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  277. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  278. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  279. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  280. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  281. package/frontend-vue/tests/module-routing.test.ts +2 -5
  282. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  283. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  284. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  285. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  286. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  287. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  288. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  289. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  290. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  291. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  292. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  293. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  294. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  295. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  296. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  297. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  298. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  299. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  300. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  301. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  302. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  303. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  304. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  305. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  306. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  307. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  308. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  309. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  310. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  311. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  312. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  313. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  314. package/frontend-vue/tests/server-routes.test.ts +582 -0
  315. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  316. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  317. package/frontend-vue/tests/side-panels.test.ts +8 -1
  318. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  319. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  320. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  321. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  322. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  323. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  324. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  325. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  326. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  327. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  328. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  329. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  330. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  331. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  332. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  333. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  334. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  335. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  336. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  337. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  338. package/frontend-vue/tests/user-language.test.ts +8 -5
  339. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  340. package/frontend-vue/vitest.config.ts +6 -0
  341. package/package.json +2 -2
  342. package/skills/dms/SKILL.md +2 -1
  343. package/skills/dms-module/SKILL.md +1 -1
  344. package/skills/dms-pages/SKILL.md +4 -1
  345. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  346. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  347. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  348. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  349. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  350. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  351. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The renderer's real data layer for a test: its async data, state and cookie
3
+ * composables, and dms-core's `useApiData` over the test's `useAuthFetch`
4
+ * stub, as the auto-imports a layer reaches. Each call starts a fresh runtime
5
+ * — a browser one under jsdom, a server one otherwise — so no data a test
6
+ * loaded leaks into the next.
7
+ */
8
+ import { createApp } from "vue";
9
+ import { vi } from "vitest";
10
+
11
+ const engine = await import("#dms/frontend-module");
12
+ const { useApiData, useApiPagedData } = await import(
13
+ "../../layers/dms-core/app/composables/auth/useApiData"
14
+ );
15
+
16
+ const ENGINE_GLOBALS = {
17
+ useDmsAsyncData: engine.useDmsAsyncData,
18
+ useDmsFetch: engine.useDmsFetch,
19
+ useDmsPagedData: engine.useDmsPagedData,
20
+ useDmsState: engine.useDmsState,
21
+ useDmsCookie: engine.useDmsCookie,
22
+ useApiData,
23
+ useApiPagedData,
24
+ };
25
+
26
+ export function useRealDataEngine(): ReturnType<
27
+ typeof engine.createDmsFrontendRuntime
28
+ > {
29
+ for (const [name, composable] of Object.entries(ENGINE_GLOBALS))
30
+ vi.stubGlobal(name, composable);
31
+ const isServer = typeof window === "undefined";
32
+ const runtime = engine.createDmsFrontendRuntime(
33
+ undefined,
34
+ undefined,
35
+ isServer,
36
+ );
37
+ if (isServer) engine.setDmsServerRuntimeResolver(() => runtime);
38
+ else
39
+ engine.provideDmsFrontendRuntime(
40
+ createApp({ render: () => null }),
41
+ runtime,
42
+ );
43
+ return runtime;
44
+ }
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { createSSRApp, defineComponent, h, ref } from "vue";
2
+ import { createSSRApp, defineComponent, h, nextTick, ref } from "vue";
3
3
  import { renderToString } from "vue/server-renderer";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import FormLoadError from "../layers/dms-ui/app/build/components/form/FormLoadError.vue";
@@ -66,6 +66,41 @@ describe("a form's record load", () => {
66
66
  expect(apply).toHaveBeenCalledWith(ROW);
67
67
  });
68
68
 
69
+ it("takes a later read of the record, its values becoming the saved ones", async () => {
70
+ const apply = vi.fn();
71
+ const onReloaded = vi.fn();
72
+ const source = loadOf(ROW, null);
73
+ trackFormRecordLoad(source, apply, { onReloaded });
74
+ const fresh = { values: { email: "fresh@example.com" }, initial: {} };
75
+ source.data.value = fresh;
76
+ await nextTick();
77
+ expect(apply).toHaveBeenLastCalledWith(fresh);
78
+ expect(onReloaded).toHaveBeenCalledOnce();
79
+ });
80
+
81
+ it("leaves a later read out while the user edits", async () => {
82
+ const apply = vi.fn();
83
+ const onReloaded = vi.fn();
84
+ const source = loadOf(ROW, null);
85
+ trackFormRecordLoad(source, apply, {
86
+ isEditing: () => true,
87
+ onReloaded,
88
+ });
89
+ source.data.value = { values: { email: "other@example.com" }, initial: {} };
90
+ await nextTick();
91
+ expect(apply).toHaveBeenCalledOnce();
92
+ expect(onReloaded).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it("stops showing the cached record once a later read is refused", async () => {
96
+ const apply = vi.fn();
97
+ const source = loadOf(ROW, null);
98
+ const load = trackFormRecordLoad(source, apply);
99
+ source.error.value = FORBIDDEN;
100
+ await nextTick();
101
+ expect(load.failure.value).toBe("forbidden");
102
+ });
103
+
69
104
  it("applies a record that loaded", () => {
70
105
  const apply = vi.fn();
71
106
  const load = trackFormRecordLoad(loadOf(ROW, null), apply);
@@ -74,6 +109,25 @@ describe("a form's record load", () => {
74
109
  });
75
110
  });
76
111
 
112
+ describe("Form.vue", () => {
113
+ const source = readFileSync(
114
+ new URL("../layers/dms-ui/app/components/form/Form.vue", import.meta.url),
115
+ "utf8",
116
+ );
117
+
118
+ it("leaves reading the record again on mount to the data layer", () => {
119
+ // The renderer reads cached data again on each mount (stale while
120
+ // revalidate): no status reset forces it any more.
121
+ expect(source).not.toMatch(/status\.value\s*=\s*"idle"/);
122
+ });
123
+
124
+ it("keys the record's load on the record it reads", () => {
125
+ expect(source).toMatch(
126
+ /`form-\$\{props\.componentId\}-\$\{props\.pageId\}-\$\{resolvedFetchUrl\.value/,
127
+ );
128
+ });
129
+ });
130
+
77
131
  describe("a form about a record its address names none of", () => {
78
132
  const FETCH_URL = "/api/tasks/get?id={{query.id}}";
79
133
 
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `useApiData` on the runtime's real async data, over a fake `$authFetch`:
3
+ * what a layer component reads, a server render of it, and the browser
4
+ * hydrating its HTML.
5
+ */
6
+ import { createApp, createSSRApp, type App, type Component } from "vue";
7
+ import { renderToString } from "vue/server-renderer";
8
+ import { vi } from "vitest";
9
+ import type { DmsFrontendRuntime, DmsPayload } from "#dms/frontend-module";
10
+
11
+ export const engine = await import("#dms/frontend-module");
12
+ const { useApiData } = await import(
13
+ "../../layers/dms-core/app/composables/auth/useApiData"
14
+ );
15
+
16
+ /** Stubs the globals `useApiData` reads, `$authFetch` being `authFetch`. */
17
+ export function stubApiData(authFetch: (...args: never[]) => unknown): void {
18
+ vi.stubGlobal("useDmsFetch", engine.useDmsFetch);
19
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
20
+ vi.stubGlobal("useDmsState", engine.useDmsState);
21
+ vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
22
+ vi.stubGlobal("useApiData", useApiData);
23
+ }
24
+
25
+ /** A browser runtime: `useApiData` outside a component reads through it. */
26
+ export function browserRuntime(app: App = createApp({})): DmsFrontendRuntime {
27
+ const runtime = engine.createDmsFrontendRuntime();
28
+ engine.provideDmsFrontendRuntime(app, runtime);
29
+ return runtime;
30
+ }
31
+
32
+ /** Renders `root` as a server would: its HTML and the payload it hands over. */
33
+ export async function serverRender(
34
+ root: Component,
35
+ setup?: (app: App) => void,
36
+ ): Promise<{ html: string; payload: DmsPayload }> {
37
+ const runtime = engine.createDmsFrontendRuntime(undefined, {}, true);
38
+ const app = createSSRApp(root);
39
+ engine.provideDmsFrontendRuntime(app, runtime);
40
+ setup?.(app);
41
+ const html = await renderToString(app);
42
+ return {
43
+ html,
44
+ payload: structuredClone(engine.serializeDmsPayload(runtime)),
45
+ };
46
+ }
47
+
48
+ /** Hydrates the server's `html` with its `payload`, as the browser does. */
49
+ export function hydrate(
50
+ root: Component,
51
+ html: string,
52
+ payload: DmsPayload,
53
+ setup?: (app: App) => void,
54
+ ): { app: App; container: HTMLElement } {
55
+ const runtime = engine.createDmsFrontendRuntime();
56
+ engine.restoreDmsPayload(runtime, payload);
57
+ const container = document.createElement("div");
58
+ container.innerHTML = html;
59
+ const app = createSSRApp(root);
60
+ engine.provideDmsFrontendRuntime(app, runtime);
61
+ setup?.(app);
62
+ app.mount(container);
63
+ return { app, container };
64
+ }
65
+
66
+ /** Mounts `root` in the browser, past the hydration (a client navigation). */
67
+ export function mountInBrowser(
68
+ root: Component,
69
+ setup?: (app: App) => void,
70
+ ): { app: App; container: HTMLElement } {
71
+ const container = document.createElement("div");
72
+ const app = createApp(root);
73
+ browserRuntime(app);
74
+ setup?.(app);
75
+ app.mount(container);
76
+ return { app, container };
77
+ }
@@ -1,19 +1,20 @@
1
1
  // @vitest-environment jsdom
2
2
  import {
3
3
  computed,
4
- createSSRApp,
5
4
  defineComponent,
6
5
  h,
7
6
  inject,
8
7
  onBeforeUnmount,
8
+ onScopeDispose,
9
9
  ref,
10
+ shallowReactive,
10
11
  useId,
11
12
  watch,
12
13
  type App,
13
14
  } from "vue";
14
- import { renderToString } from "vue/server-renderer";
15
15
  import { afterEach, beforeEach, expect, it, vi } from "vitest";
16
16
  import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
17
+ import { hydrate, serverRender, stubApiData } from "./helpers/api-data-runtime";
17
18
 
18
19
  const STORED_IMAGE = { key: "files/profile/cover.png", alt: "Cover" };
19
20
 
@@ -25,16 +26,14 @@ const passthrough = defineComponent({
25
26
  slots.default?.(),
26
27
  });
27
28
 
28
- function createImageApp(): App {
29
- const app = createSSRApp({
30
- render: () => h(Image, { modelValue: STORED_IMAGE }),
31
- });
29
+ const Root = { render: () => h(Image, { modelValue: STORED_IMAGE }) };
30
+
31
+ function register(app: App): void {
32
32
  app.component("UButton", defineComponent({ setup: () => () => h("button") }));
33
33
  app.component("UIcon", defineComponent({ setup: () => () => h("span") }));
34
34
  app.component("UInput", defineComponent({ setup: () => () => h("input") }));
35
35
  app.component("UTooltip", passthrough);
36
36
  app.component("UModal", defineComponent({ setup: () => () => null }));
37
- return app;
38
37
  }
39
38
 
40
39
  beforeEach(() => {
@@ -44,12 +43,17 @@ beforeEach(() => {
44
43
  vi.stubGlobal("inject", inject);
45
44
  vi.stubGlobal("useId", useId);
46
45
  vi.stubGlobal("onBeforeUnmount", onBeforeUnmount);
46
+ vi.stubGlobal("onScopeDispose", onScopeDispose);
47
+ vi.stubGlobal("shallowReactive", shallowReactive);
47
48
  vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
48
49
  vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
49
50
  vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
50
- vi.stubGlobal("useAuthFetch", () => ({
51
- $authFetch: () => new Promise(() => undefined),
52
- }));
51
+ stubApiData((async () => ({
52
+ resourceKey: STORED_IMAGE.key,
53
+ filename: "cover.png",
54
+ size: 10,
55
+ url: "https://files.test/cover.png",
56
+ })) as never);
53
57
  vi.stubGlobal("useUploadWithProgress", () => ({
54
58
  uploadWithProgress: vi.fn(),
55
59
  }));
@@ -72,15 +76,16 @@ afterEach(() => {
72
76
  });
73
77
 
74
78
  it("hydrates a stored single image without an attribute mismatch", async () => {
75
- const html = await renderToString(createImageApp());
76
- const container = document.createElement("div");
77
- container.innerHTML = html;
79
+ vi.stubEnv("SSR", true);
80
+ const { html, payload } = await serverRender(Root, register);
81
+ vi.unstubAllEnvs();
78
82
  const warnings: string[] = [];
79
- const app = createImageApp();
80
- app.config.warnHandler = (message) => warnings.push(message);
81
83
  const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
82
84
 
83
- app.mount(container);
85
+ const { app, container } = hydrate(Root, html, payload, (application) => {
86
+ register(application);
87
+ application.config.warnHandler = (message) => warnings.push(message);
88
+ });
84
89
 
85
90
  expect(warnings.filter((message) => message.includes("Hydration"))).toEqual(
86
91
  [],
@@ -2,30 +2,29 @@ import { readFileSync } from "node:fs";
2
2
  import { transpileModule } from "typescript";
3
3
  import {
4
4
  computed,
5
- createSSRApp,
5
+ createApp,
6
6
  defineComponent,
7
7
  effectScope,
8
8
  getCurrentInstance,
9
9
  h,
10
10
  nextTick,
11
11
  onScopeDispose,
12
- onServerPrefetch,
13
12
  reactive,
14
13
  ref,
14
+ shallowReactive,
15
15
  watch,
16
16
  useId,
17
+ type App,
17
18
  type EffectScope,
18
19
  type Ref,
19
20
  } from "vue";
20
- import { renderToString } from "vue/server-renderer";
21
21
  import { afterEach, beforeEach, expect, it, vi } from "vitest";
22
22
  import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
23
23
  import { useKeyboardPlatform } from "../layers/dms-ui/app/composables/global/keyboardPlatform";
24
24
  import { fieldErrorId } from "../layers/dms-core/app/composables/useFieldErrors";
25
- import {
26
- isDefinitiveMetadataFailure,
27
- resolveMetadataRetryDelay,
28
- } from "../layers/dms-ui/app/utils/metadataRetry";
25
+ import { useFileMetadata } from "../layers/dms-ui/app/build/composables/files/useFileMetadata";
26
+ import { FORM_CONTENT_LANGUAGE_KEY } from "../layers/dms-ui/app/composables/form/types/content-language";
27
+ import { engine, serverRender, stubApiData } from "./helpers/api-data-runtime";
29
28
 
30
29
  interface ImageValue {
31
30
  key: string;
@@ -47,13 +46,14 @@ interface ImageHarness {
47
46
  emitValue: () => void;
48
47
  removeItem: (id: string) => void;
49
48
  addFiles: (files: File[]) => void;
50
- fetchItemMetadata: (item: GalleryEntry, force?: boolean) => Promise<void>;
51
49
  }
52
50
 
53
51
  const emit = vi.fn();
54
52
  const authFetch = vi.fn();
55
53
  const upload = vi.fn();
56
54
  let scope: EffectScope;
55
+ // The application the script runs in: its DMS runtime reads the metadata.
56
+ let app: App;
57
57
  const props = reactive({ modelValue: [] as ImageValue[], multiple: true });
58
58
  const original: ImageValue = {
59
59
  key: "legacy/private.png",
@@ -78,15 +78,26 @@ function loadImage(): ImageHarness {
78
78
  .replaceAll("import.meta.env.SSR", "false")
79
79
  .replace(/^export type /gm, "type ");
80
80
  const { outputText } = transpileModule(source, {});
81
- return scope.run(() =>
82
- new Function(
83
- `${outputText}; return { items, emitValue, removeItem, addFiles, fetchItemMetadata };`,
84
- )(),
81
+ return app.runWithContext(() =>
82
+ scope.run(() =>
83
+ new Function(
84
+ `${outputText}; return { items, emitValue, removeItem, addFiles };`,
85
+ )(),
86
+ ),
85
87
  )!;
86
88
  }
87
89
 
90
+ /** Lets the metadata reads answer. */
91
+ const settle = async () => {
92
+ for (let tick = 0; tick < 5; tick++) await nextTick();
93
+ await new Promise((done) => setTimeout(done, 0));
94
+ };
95
+
88
96
  beforeEach(() => {
89
97
  scope = effectScope();
98
+ app = createApp({});
99
+ engine.provideDmsFrontendRuntime(app, engine.createDmsFrontendRuntime());
100
+ vi.stubEnv("SSR", false);
90
101
  props.modelValue = [original, sibling];
91
102
  authFetch.mockImplementation((_url, options) =>
92
103
  options.query?.resourceKey === original.key
@@ -98,8 +109,8 @@ beforeEach(() => {
98
109
  }),
99
110
  );
100
111
  vi.stubGlobal("defineProps", () => props);
101
- vi.stubGlobal("isDefinitiveMetadataFailure", isDefinitiveMetadataFailure);
102
- vi.stubGlobal("resolveMetadataRetryDelay", resolveMetadataRetryDelay);
112
+ vi.stubGlobal("useFileMetadata", useFileMetadata);
113
+ vi.stubGlobal("shallowReactive", shallowReactive);
103
114
  vi.stubGlobal("defineEmits", () => emit);
104
115
  vi.stubGlobal("ref", ref);
105
116
  vi.stubGlobal("computed", computed);
@@ -107,10 +118,9 @@ beforeEach(() => {
107
118
  vi.stubGlobal("inject", () => null);
108
119
  vi.stubGlobal("useId", () => (getCurrentInstance() ? useId() : "image"));
109
120
  vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
110
- vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
111
121
  vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
112
122
  vi.stubGlobal("onBeforeUnmount", onScopeDispose);
113
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
123
+ stubApiData(authFetch);
114
124
  vi.stubGlobal("useUploadWithProgress", () => ({
115
125
  uploadWithProgress: upload,
116
126
  }));
@@ -135,13 +145,14 @@ beforeEach(() => {
135
145
 
136
146
  afterEach(() => {
137
147
  scope.stop();
148
+ vi.unstubAllEnvs();
138
149
  vi.unstubAllGlobals();
139
150
  vi.resetAllMocks();
140
151
  });
141
152
 
142
153
  it("preserves a denied image when another image is removed", async () => {
143
154
  const image = loadImage();
144
- await nextTick();
155
+ await settle();
145
156
  expect(image.items.value[0]!.status).toBe("unavailable");
146
157
  image.removeItem(image.items.value[1]!.id);
147
158
  expect(emit).toHaveBeenLastCalledWith("update:modelValue", [original]);
@@ -149,7 +160,7 @@ it("preserves a denied image when another image is removed", async () => {
149
160
 
150
161
  it("preserves a denied image on value echo without reinitializing its identity", async () => {
151
162
  const image = loadImage();
152
- await nextTick();
163
+ await settle();
153
164
  const id = image.items.value[0]!.id;
154
165
  props.modelValue = [{ ...original }, { ...sibling }];
155
166
  await nextTick();
@@ -163,7 +174,7 @@ it("preserves a denied image on value echo without reinitializing its identity",
163
174
 
164
175
  it("removes a denied reference only when explicitly removed", async () => {
165
176
  const image = loadImage();
166
- await nextTick();
177
+ await settle();
167
178
  image.removeItem(image.items.value[0]!.id);
168
179
  expect(emit).toHaveBeenLastCalledWith("update:modelValue", [
169
180
  { ...sibling, principal: true },
@@ -172,7 +183,7 @@ it("removes a denied reference only when explicitly removed", async () => {
172
183
 
173
184
  it("excludes a failed new upload while retaining existing references", async () => {
174
185
  const image = loadImage();
175
- await nextTick();
186
+ await settle();
176
187
  authFetch.mockRejectedValueOnce(new Error("Upload failed"));
177
188
  image.addFiles([new File(["bytes"], "new.png", { type: "image/png" })]);
178
189
  await vi.waitFor(() =>
@@ -187,35 +198,34 @@ it("excludes a failed new upload while retaining existing references", async ()
187
198
  });
188
199
 
189
200
  it("keeps the denied image and its explicit deletion control in rendered markup", async () => {
190
- vi.stubGlobal("useAuthFetch", () => ({
191
- $authFetch: () => {
192
- const denied = Promise.reject(new Error("403"));
193
- onServerPrefetch(() => denied.catch(() => undefined));
194
- return denied;
195
- },
196
- }));
197
- const app = createSSRApp({
198
- render: () => h(Image, { modelValue: [original], multiple: true }),
199
- });
200
- app.component(
201
- "UButton",
202
- defineComponent({
203
- props: ["label"],
204
- setup: (buttonProps) => () => h("button", buttonProps.label),
205
- }),
201
+ authFetch.mockRejectedValue(
202
+ Object.assign(new Error("Forbidden"), { statusCode: 403 }),
206
203
  );
207
- app.component("UIcon", defineComponent({ setup: () => () => h("span") }));
208
- app.component(
209
- "UTooltip",
210
- defineComponent({
211
- setup:
212
- (_, { slots }) =>
213
- () =>
214
- slots.default?.(),
215
- }),
204
+ const register = (app: App) => {
205
+ app.component(
206
+ "UButton",
207
+ defineComponent({
208
+ props: ["label"],
209
+ setup: (buttonProps) => () => h("button", buttonProps.label),
210
+ }),
211
+ );
212
+ app.component("UIcon", defineComponent({ setup: () => () => h("span") }));
213
+ app.component(
214
+ "UTooltip",
215
+ defineComponent({
216
+ setup:
217
+ (_, { slots }) =>
218
+ () =>
219
+ slots.default?.(),
220
+ }),
221
+ );
222
+ app.component("UModal", defineComponent({ setup: () => () => null }));
223
+ };
224
+ vi.stubEnv("SSR", true);
225
+ const { html } = await serverRender(
226
+ { render: () => h(Image, { modelValue: [original], multiple: true }) },
227
+ register,
216
228
  );
217
- app.component("UModal", defineComponent({ setup: () => () => null }));
218
- const html = await renderToString(app);
219
229
  expect(html).toContain("dms.form.image.file_unavailable");
220
230
  expect(html).not.toContain("dms.form.image.upload_failed");
221
231
  expect(html).toContain("dms.form.image.delete</button>");
@@ -225,11 +235,7 @@ it("keeps the denied image and its explicit deletion control in rendered markup"
225
235
  it("refreshes an expiring image preview and retries a transient failure", async () => {
226
236
  vi.useFakeTimers();
227
237
  vi.setSystemTime(new Date("2026-09-14T12:00:00Z"));
228
- const contentLanguageKey = Symbol();
229
- vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", contentLanguageKey);
230
- vi.stubGlobal("inject", (key: symbol) =>
231
- key === contentLanguageKey ? "*" : null,
232
- );
238
+ app.provide(FORM_CONTENT_LANGUAGE_KEY, "*");
233
239
  props.modelValue = [original];
234
240
  authFetch
235
241
  .mockReset()
@@ -248,7 +254,7 @@ it("refreshes an expiring image preview and retries a transient failure", async
248
254
  });
249
255
 
250
256
  const image = loadImage();
251
- await nextTick();
257
+ await vi.advanceTimersByTimeAsync(0);
252
258
  await vi.advanceTimersByTimeAsync(55000);
253
259
  expect(image.items.value[0]!.url).toBe("https://example.test/first.png");
254
260
  await vi.advanceTimersByTimeAsync(1000);
@@ -78,9 +78,23 @@ async function mount(props: Record<string, unknown>) {
78
78
  app.component("UInputNumber", NumberField);
79
79
  app.mount(host);
80
80
  await nextTick();
81
+ await untilListenersAttached();
81
82
  return { value, adjustments, input: host.querySelector("input")! };
82
83
  }
83
84
 
85
+ // Vue ignores an event that reaches one of its listeners in the millisecond
86
+ // that listener was attached (runtime-dom's `_vts` check, against events
87
+ // fired while the patch that attached it runs). Enter bubbles from the field's
88
+ // own listener to the wrapper's, so an Enter fired in the millisecond of the
89
+ // mount never settled the field: about one run in two. A person never types
90
+ // that fast, so the field is used once the clock has moved on.
91
+ async function untilListenersAttached() {
92
+ const attached = Date.now();
93
+ while (Date.now() <= attached) {
94
+ await new Promise((resolve) => setTimeout(resolve, 1));
95
+ }
96
+ }
97
+
84
98
  async function type(input: HTMLInputElement, text: string) {
85
99
  input.value = text;
86
100
  input.dispatchEvent(new Event("input", { bubbles: true }));
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { createApp, defineComponent, h, reactive, ref } from "vue";
8
8
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { useRealDataEngine } from "./fixtures/dataEngine";
9
10
  import { createI18n } from "vue-i18n";
10
11
 
11
12
  const i18n = createI18n({
@@ -66,6 +67,8 @@ function copyButtons(root: HTMLElement): HTMLButtonElement[] {
66
67
  return [...root.querySelectorAll<HTMLButtonElement>("button[title]")];
67
68
  }
68
69
 
70
+ beforeEach(() => useRealDataEngine());
71
+
69
72
  beforeEach(() => {
70
73
  fetchedItems = [];
71
74
  copied.splice(0);
@@ -16,6 +16,7 @@ import {
16
16
  formStateInjectionKey,
17
17
  } from "@nuxt/ui/composables/useFormField";
18
18
  import { sameFormValue } from "../layers/dms-ui/app/build/composables/unsaved-changes/formValue";
19
+ import { browserRuntime, stubApiData } from "./helpers/api-data-runtime";
19
20
 
20
21
  const ROLES_URL = "/settings/workspace/members/role-options";
21
22
  const ROLE_OPTIONS = {
@@ -74,6 +75,7 @@ async function mountPicker(props: Record<string, unknown> = {}) {
74
75
  },
75
76
  });
76
77
  app = createApp(Field);
78
+ browserRuntime(app);
77
79
  const stub = (tag: string) =>
78
80
  defineComponent({
79
81
  setup:
@@ -110,7 +112,7 @@ beforeEach(() => {
110
112
  vi.stubGlobal("useI18n", () => ({ t: translate }));
111
113
  authFetch.mockReset();
112
114
  authFetch.mockResolvedValue(ROLE_OPTIONS);
113
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
115
+ stubApiData(authFetch);
114
116
  fieldError.value = undefined;
115
117
  formState.value = {};
116
118
  model.value = [];
@@ -127,7 +129,7 @@ afterEach(() => {
127
129
  it("offers the server's roles and picks and unpicks them", async () => {
128
130
  await mountPicker();
129
131
 
130
- expect(authFetch).toHaveBeenCalledWith(ROLES_URL);
132
+ expect(authFetch).toHaveBeenCalledWith(ROLES_URL, expect.anything());
131
133
  expect(
132
134
  [...host.querySelectorAll("button[data-value]")].map((b) =>
133
135
  b.textContent?.trim(),
@@ -0,0 +1,47 @@
1
+ import { nextTick, ref } from "vue";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ vi.stubGlobal("defineDmsPlugin", (setup: unknown) => setup);
5
+ const { watchRealtimeReconnects } = await import(
6
+ "../layers/dms-layout/app/plugins/dms-menu-sync.client"
7
+ );
8
+
9
+ // A realtime stream that drops and opens again missed the menu events sent
10
+ // meanwhile: the menu resyncs once it is back, but not on the first opening,
11
+ // which follows the page load that brought the current menu.
12
+
13
+ describe("the menu resync on a realtime reconnect", () => {
14
+ it("leaves the first opening alone and resyncs on each reopening", async () => {
15
+ const sessionId = ref<string | undefined>(undefined);
16
+ const onReconnect = vi.fn();
17
+ watchRealtimeReconnects(sessionId, onReconnect);
18
+
19
+ sessionId.value = "session-1";
20
+ await nextTick();
21
+ expect(onReconnect).not.toHaveBeenCalled();
22
+
23
+ sessionId.value = undefined;
24
+ await nextTick();
25
+ sessionId.value = "session-2";
26
+ await nextTick();
27
+ expect(onReconnect).toHaveBeenCalledOnce();
28
+
29
+ sessionId.value = undefined;
30
+ await nextTick();
31
+ sessionId.value = "session-3";
32
+ await nextTick();
33
+ expect(onReconnect).toHaveBeenCalledTimes(2);
34
+ });
35
+
36
+ it("counts a stream already open when it starts as its first opening", async () => {
37
+ const sessionId = ref<string | undefined>("session-1");
38
+ const onReconnect = vi.fn();
39
+ watchRealtimeReconnects(sessionId, onReconnect);
40
+
41
+ sessionId.value = undefined;
42
+ await nextTick();
43
+ sessionId.value = "session-2";
44
+ await nextTick();
45
+ expect(onReconnect).toHaveBeenCalledOnce();
46
+ });
47
+ });
@@ -10,12 +10,14 @@ import {
10
10
  onBeforeUnmount,
11
11
  onScopeDispose,
12
12
  ref,
13
+ shallowReactive,
13
14
  useId,
14
15
  watch,
15
16
  } from "vue";
16
17
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
18
  import File from "../layers/dms-ui/app/components/form/components/File.vue";
18
19
  import Image from "../layers/dms-ui/app/components/form/components/Image.vue";
20
+ import { browserRuntime, stubApiData } from "./helpers/api-data-runtime";
19
21
  import {
20
22
  isDefinitiveMetadataFailure,
21
23
  resolveMetadataRetryDelay,
@@ -80,6 +82,7 @@ function mountField(field: FieldCase): { app: App; container: HTMLElement } {
80
82
  const app = createApp({
81
83
  render: () => h(field.component, { modelValue: field.modelValue }),
82
84
  });
85
+ browserRuntime(app);
83
86
  app.config.globalProperties.$t = (key: string) => key;
84
87
  for (const name of ["UFileUpload", "ULink", "UTooltip"])
85
88
  app.component(name, passthrough);
@@ -104,7 +107,8 @@ beforeEach(() => {
104
107
  vi.stubGlobal("FORM_FIELD_LOADING_KEY", Symbol());
105
108
  vi.stubGlobal("FORM_CONTENT_LANGUAGE_KEY", Symbol());
106
109
  vi.stubGlobal("CONTENT_LANGUAGE_HEADER", "x-content-language");
107
- vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
110
+ vi.stubGlobal("shallowReactive", shallowReactive);
111
+ stubApiData(authFetch);
108
112
  vi.stubGlobal("useUploadWithProgress", () => ({
109
113
  uploadWithProgress: vi.fn(),
110
114
  }));