@antelopejs/dms 0.8.2 → 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 (354) hide show
  1. package/CHANGELOG.md +36 -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/interfaces/dms-base/table-and-card-catalog.test.js +25 -0
  118. package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js.map +1 -1
  119. package/dist/test/unit/pages/settings/member-roles-field.test.js +1 -1
  120. package/dist/test/unit/pages/settings/member-roles-field.test.js.map +1 -1
  121. package/dist/test/unit/pages/settings/role-delete-confirm.test.js.map +1 -1
  122. package/dist/test/unit/realtime/menu-topics.test.d.ts +1 -0
  123. package/dist/test/unit/realtime/menu-topics.test.js +26 -0
  124. package/dist/test/unit/realtime/menu-topics.test.js.map +1 -0
  125. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  126. package/frontend-vue/deferred-plugins.ts +3 -4
  127. package/frontend-vue/dms.email.ts +5 -1
  128. package/frontend-vue/dms.frontend.ts +15 -5
  129. package/frontend-vue/dms.server.ts +74 -0
  130. package/frontend-vue/layers/dms-auth/app/composables/useLogout.ts +1 -1
  131. package/frontend-vue/layers/dms-auth/app/composables/usePostLoginRedirect.ts +16 -3
  132. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/2fa.vue +3 -1
  133. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/backup.vue +3 -1
  134. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/forgot-validation.vue +1 -1
  135. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/oauth-complete.vue +26 -3
  136. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/recover.vue +1 -1
  137. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +22 -25
  138. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/validate.vue +1 -1
  139. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -7
  140. package/frontend-vue/layers/dms-auth/app/plugins/permissions-preload.ts +17 -0
  141. package/frontend-vue/layers/dms-auth/server/accounts.ts +135 -0
  142. package/frontend-vue/layers/dms-auth/server/actions.ts +251 -0
  143. package/frontend-vue/layers/dms-auth/server/oauth.ts +260 -0
  144. package/frontend-vue/layers/dms-auth/server/session.ts +158 -0
  145. package/frontend-vue/layers/dms-core/app/build/utils/asyncDataScope.ts +47 -0
  146. package/frontend-vue/layers/dms-core/app/build/utils/permission-preview.ts +6 -2
  147. package/frontend-vue/layers/dms-core/app/composables/auth/useApiData.ts +148 -0
  148. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +7 -20
  149. package/frontend-vue/layers/dms-core/app/composables/auth/useCurrentUser.ts +2 -2
  150. package/frontend-vue/layers/dms-core/app/composables/auth/useMultiAccount.ts +18 -5
  151. package/frontend-vue/layers/dms-core/app/composables/auth/usePermissions.ts +35 -6
  152. package/frontend-vue/layers/dms-core/app/composables/auth/useSessionRecovery.ts +1 -1
  153. package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +20 -14
  154. package/frontend-vue/layers/dms-core/app/composables/period/periodClock.ts +121 -0
  155. package/frontend-vue/layers/dms-core/app/composables/period/periodRenderNow.ts +42 -0
  156. package/frontend-vue/layers/dms-core/app/composables/period/periodTimeZone.ts +31 -0
  157. package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +48 -19
  158. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +33 -4
  159. package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +78 -25
  160. package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +3 -0
  161. package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +36 -6
  162. package/frontend-vue/layers/dms-core/app/composables/user/useUserRegionalPreferences.ts +1 -1
  163. package/frontend-vue/layers/dms-core/app/plugins/async-data-scope.ts +26 -0
  164. package/frontend-vue/layers/dms-core/app/utils/pageRoutes.ts +22 -0
  165. package/frontend-vue/layers/dms-core/app/utils/regional.ts +36 -3
  166. package/frontend-vue/layers/dms-core/app/utils/trailingDeduper.ts +10 -1
  167. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +48 -4
  168. package/frontend-vue/layers/dms-layout/app/app.config.ts +42 -23
  169. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +72 -20
  170. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardFrame.vue +9 -0
  171. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +0 -3
  172. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +9 -3
  173. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/members/MemberRolePicker.vue +13 -12
  174. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +17 -7
  175. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationPreferencesForm.vue +0 -1
  176. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +36 -33
  177. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfileSecuritySummary.vue +1 -2
  178. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/roles/role-types.ts +5 -4
  179. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +35 -28
  180. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityStatus.vue +0 -1
  181. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +1 -1
  182. package/frontend-vue/layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder.ts +7 -6
  183. package/frontend-vue/layers/dms-layout/app/build/composables/layout/sidePanelState.ts +1 -1
  184. package/frontend-vue/layers/dms-layout/app/build/composables/navigation/useVisitNavBadges.ts +26 -0
  185. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationCatalog.ts +25 -23
  186. package/frontend-vue/layers/dms-layout/app/build/composables/notification/useNotificationPreferences.ts +44 -23
  187. package/frontend-vue/layers/dms-layout/app/build/composables/security/useSessionHandoff.ts +1 -1
  188. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/preferencesSummary.ts +1 -1
  189. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +3 -2
  190. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfileAvatar.ts +1 -18
  191. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useProfilePreferences.ts +19 -23
  192. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +6 -3
  193. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityOverview.ts +13 -18
  194. package/frontend-vue/layers/dms-layout/app/build/utils/modules-catalog.ts +8 -6
  195. package/frontend-vue/layers/dms-layout/app/build/utils/publicErrorMessage.ts +19 -0
  196. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -11
  197. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +3 -13
  198. package/frontend-vue/layers/dms-layout/app/composables/general/useSystemState.ts +7 -11
  199. package/frontend-vue/layers/dms-layout/app/composables/notification/useNotifications.ts +103 -22
  200. package/frontend-vue/layers/dms-layout/app/composables/page/useModulesListing.ts +25 -102
  201. package/frontend-vue/layers/dms-layout/app/composables/page/usePageComponentPreload.ts +0 -10
  202. package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +177 -96
  203. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +3 -2
  204. package/frontend-vue/layers/dms-layout/app/custom-pages/modules.vue +6 -28
  205. package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +31 -11
  206. package/frontend-vue/layers/dms-layout/app/error.vue +5 -2
  207. package/frontend-vue/layers/dms-layout/app/middleware/module-routing.global.ts +1 -1
  208. package/frontend-vue/layers/dms-layout/app/middleware/page-leave-guard.global.ts +2 -2
  209. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +12 -9
  210. package/frontend-vue/layers/dms-layout/app/plugins/dms-menu-sync.client.ts +31 -3
  211. package/frontend-vue/layers/dms-layout/app/plugins/favicon.ts +58 -0
  212. package/frontend-vue/layers/dms-layout/app/plugins/notification-stream.client.ts +15 -7
  213. package/frontend-vue/layers/dms-layout/app/plugins/profile-sync.client.ts +5 -1
  214. package/frontend-vue/layers/dms-layout/app/plugins/regional-preferences.ts +10 -4
  215. package/frontend-vue/layers/dms-layout/app/plugins/time-zone-cookie.client.ts +12 -0
  216. package/frontend-vue/layers/dms-layout/app/types/page.ts +13 -0
  217. package/frontend-vue/layers/dms-layout/server/html-render.ts +58 -0
  218. package/frontend-vue/layers/dms-layout/server/render-token.ts +95 -0
  219. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/useOnboarding.ts +25 -21
  220. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +1 -1
  221. package/frontend-vue/layers/dms-onboarding/app/plugins/onboarding-preload.ts +9 -0
  222. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +4 -1
  223. package/frontend-vue/layers/dms-ui/app/build/composables/files/useFileMetadata.ts +162 -0
  224. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +23 -4
  225. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageDataRefresh.ts +101 -0
  226. package/frontend-vue/layers/dms-ui/app/build/composables/navigation/useNavBadges.ts +33 -6
  227. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +19 -16
  228. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +40 -20
  229. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableFooter.ts +6 -8
  230. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +42 -14
  231. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +19 -8
  232. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +1 -1
  233. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +5 -2
  234. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +9 -9
  235. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +20 -18
  236. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -1
  237. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +26 -132
  238. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +40 -105
  239. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -0
  240. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -4
  241. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +9 -13
  242. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +2 -1
  243. package/frontend-vue/layers/dms-ui/app/components/table-view/FilePreview.vue +12 -24
  244. package/frontend-vue/layers/dms-ui/app/components/table-view/ImagePreview.vue +17 -29
  245. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +7 -3
  246. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +20 -8
  247. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +8 -11
  248. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +146 -182
  249. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLiveRefresh.ts +8 -2
  250. package/frontend-vue/layers/dms-ui/app/composables/useFileReadUrls.ts +11 -99
  251. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +32 -3
  252. package/frontend-vue/package.json +2 -2
  253. package/frontend-vue/pnpm-lock.yaml +5 -5
  254. package/frontend-vue/server/private-options.ts +27 -0
  255. package/frontend-vue/tests/async-data-scope.test.ts +121 -0
  256. package/frontend-vue/tests/auth-middleware-error.test.ts +3 -3
  257. package/frontend-vue/tests/banner-block-fetch.test.ts +3 -0
  258. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +3 -0
  259. package/frontend-vue/tests/block-items-period-realtime.test.ts +3 -0
  260. package/frontend-vue/tests/block-not-found.test.ts +16 -2
  261. package/frontend-vue/tests/block-page-refresh.test.ts +3 -0
  262. package/frontend-vue/tests/block-route-params.test.ts +3 -0
  263. package/frontend-vue/tests/branding-defaults.test.ts +34 -0
  264. package/frontend-vue/tests/builtin-emails.test.ts +23 -2
  265. package/frontend-vue/tests/chart-live-pause.test.ts +4 -1
  266. package/frontend-vue/tests/chart-value-propagation.test.ts +4 -1
  267. package/frontend-vue/tests/composed-text-blocks.test.ts +3 -0
  268. package/frontend-vue/tests/email-layout.test.ts +66 -4
  269. package/frontend-vue/tests/error-page-message.test.ts +25 -2
  270. package/frontend-vue/tests/favicon.test.ts +131 -0
  271. package/frontend-vue/tests/file-fields-ssr.test.ts +145 -0
  272. package/frontend-vue/tests/file-read-urls.test.ts +145 -34
  273. package/frontend-vue/tests/file-unavailable.test.ts +5 -1
  274. package/frontend-vue/tests/fixtures/dataEngine.ts +44 -0
  275. package/frontend-vue/tests/form-load-error.test.ts +55 -1
  276. package/frontend-vue/tests/helpers/api-data-runtime.ts +77 -0
  277. package/frontend-vue/tests/image-hydration-ids.test.ts +21 -16
  278. package/frontend-vue/tests/image-reference-preservation.test.ts +59 -53
  279. package/frontend-vue/tests/input-number-adjust.test.ts +14 -0
  280. package/frontend-vue/tests/key-value-list-copy.test.ts +3 -0
  281. package/frontend-vue/tests/member-role-picker.test.ts +4 -2
  282. package/frontend-vue/tests/menu-sync-reconnect.test.ts +47 -0
  283. package/frontend-vue/tests/metadata-retry.test.ts +5 -1
  284. package/frontend-vue/tests/module-routing.test.ts +2 -5
  285. package/frontend-vue/tests/modules-catalog.test.ts +17 -4
  286. package/frontend-vue/tests/modules-listing-transfer.test.ts +95 -0
  287. package/frontend-vue/tests/multi-account-validate.test.ts +23 -1
  288. package/frontend-vue/tests/nav-badge-tones.test.ts +66 -3
  289. package/frontend-vue/tests/notification-counts-from-writes.test.ts +223 -0
  290. package/frontend-vue/tests/notification-counts-transfer.test.ts +88 -0
  291. package/frontend-vue/tests/notification-inbox-display.test.ts +57 -5
  292. package/frontend-vue/tests/notification-popover-errors.test.ts +21 -1
  293. package/frontend-vue/tests/oauth-complete-handoff.test.ts +209 -0
  294. package/frontend-vue/tests/onboarding-middleware.test.ts +1 -1
  295. package/frontend-vue/tests/onboarding-transfer.test.ts +106 -0
  296. package/frontend-vue/tests/page-data-refresh.test.ts +141 -0
  297. package/frontend-vue/tests/page-fill-height.test.ts +16 -3
  298. package/frontend-vue/tests/page-no-visible-blocks.test.ts +12 -2
  299. package/frontend-vue/tests/page-realtime-remount.test.ts +1 -1
  300. package/frontend-vue/tests/page-record-label.test.ts +0 -7
  301. package/frontend-vue/tests/page-validation-redirect.test.ts +12 -2
  302. package/frontend-vue/tests/period-hydration-now.test.ts +185 -0
  303. package/frontend-vue/tests/period-live-pause.test.ts +3 -0
  304. package/frontend-vue/tests/period-reader-zone.test.ts +137 -0
  305. package/frontend-vue/tests/period-scope-first-fetch.test.ts +17 -1
  306. package/frontend-vue/tests/period-ssr.test.ts +195 -0
  307. package/frontend-vue/tests/permission-preview-partial.test.ts +0 -2
  308. package/frontend-vue/tests/permissions-transfer.test.ts +194 -0
  309. package/frontend-vue/tests/post-login-redirect.test.ts +10 -5
  310. package/frontend-vue/tests/profile-personal-info-ssr.test.ts +150 -0
  311. package/frontend-vue/tests/profile-preferences-summary.test.ts +15 -11
  312. package/frontend-vue/tests/realtime-reconnect.test.ts +22 -3
  313. package/frontend-vue/tests/roles-name-errors.test.ts +1 -1
  314. package/frontend-vue/tests/row-side-panel.test.ts +1 -1
  315. package/frontend-vue/tests/security-sessions-load-error.test.ts +3 -1
  316. package/frontend-vue/tests/server-routes-vs-pages.test.ts +26 -0
  317. package/frontend-vue/tests/server-routes.test.ts +582 -0
  318. package/frontend-vue/tests/session-handoff.test.ts +1 -1
  319. package/frontend-vue/tests/settings-reads-ssr.test.ts +232 -0
  320. package/frontend-vue/tests/side-panels.test.ts +8 -1
  321. package/frontend-vue/tests/sign-in-exit-history.test.ts +219 -0
  322. package/frontend-vue/tests/signup-invitation-link.test.ts +56 -8
  323. package/frontend-vue/tests/site-layout-categories.test.ts +42 -0
  324. package/frontend-vue/tests/site-layout-hydration.test.ts +3 -3
  325. package/frontend-vue/tests/site-layout-rendered.test.ts +103 -0
  326. package/frontend-vue/tests/site-layout-resync.test.ts +87 -0
  327. package/frontend-vue/tests/ssr-guard-preload.test.ts +188 -0
  328. package/frontend-vue/tests/table-cell-tones.test.ts +36 -1
  329. package/frontend-vue/tests/table-data-ssr.test.ts +195 -0
  330. package/frontend-vue/tests/table-list-data-requests.test.ts +154 -0
  331. package/frontend-vue/tests/table-retry-loading.test.ts +37 -0
  332. package/frontend-vue/tests/table-source-navigation.test.ts +0 -1
  333. package/frontend-vue/tests/table-source-period-scope.test.ts +0 -1
  334. package/frontend-vue/tests/table-view-views.test.ts +1 -1
  335. package/frontend-vue/tests/top-list-card-actions.test.ts +3 -0
  336. package/frontend-vue/tests/top-list-card-period-badge.test.ts +79 -0
  337. package/frontend-vue/tests/top-list-card-value-suffix.test.ts +3 -0
  338. package/frontend-vue/tests/trailing-deduper.test.ts +22 -0
  339. package/frontend-vue/tests/use-api-data-update.test.ts +119 -0
  340. package/frontend-vue/tests/use-api-data.test.ts +410 -0
  341. package/frontend-vue/tests/user-language.test.ts +8 -5
  342. package/frontend-vue/tests/widget-label-i18n.test.ts +4 -1
  343. package/frontend-vue/vitest.config.ts +6 -0
  344. package/package.json +2 -2
  345. package/skills/dms/SKILL.md +2 -1
  346. package/skills/dms-module/SKILL.md +1 -1
  347. package/skills/dms-pages/SKILL.md +4 -1
  348. package/frontend-vue/layers/dms-core/app/build/composables/page/usePageRecordLabel.ts +0 -9
  349. package/frontend-vue/layers/dms-layout/app/composables/page/usePrefetch.ts +0 -21
  350. package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +0 -10
  351. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useServerRenderedAsyncData.ts +0 -23
  352. package/frontend-vue/tests/control-error-path.test.ts +0 -11
  353. package/frontend-vue/tests/modules-listing-per-request.test.ts +0 -104
  354. package/frontend-vue/tests/page-layout-background-refresh.test.ts +0 -321
@@ -21,6 +21,16 @@ import {
21
21
  usePageFillHeight,
22
22
  } from "../layers/dms-layout/app/composables/layout/usePageFillHeight";
23
23
 
24
+ // The page's data refresh and the visit's navigation badges read the
25
+ // realtime session and Inertia's page, which these tests do not run.
26
+ vi.mock(
27
+ "../layers/dms-ui/app/build/composables/live/usePageDataRefresh",
28
+ () => ({ usePageDataRefresh: () => {} }),
29
+ );
30
+ vi.mock(
31
+ "../layers/dms-layout/app/build/composables/navigation/useVisitNavBadges",
32
+ () => ({ useVisitNavBadges: () => {} }),
33
+ );
24
34
  vi.mock(
25
35
  "../layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder",
26
36
  async () => {
@@ -73,7 +83,7 @@ function installRuntime(): void {
73
83
  PAGE_FILL_HEIGHT_CLASSES,
74
84
  providePageFillHeight,
75
85
  usePageFillHeight,
76
- createError: vi.fn(),
86
+ createDmsError: vi.fn(),
77
87
  defineDmsPageMeta: vi.fn(),
78
88
  firstAccessiblePagePath: vi.fn(),
79
89
  navigateDms,
@@ -81,7 +91,7 @@ function installRuntime(): void {
81
91
  providePageRealtime: vi.fn(),
82
92
  resolveDmsComponent: () => "div",
83
93
  runCustomValidation: vi.fn(),
84
- showError: vi.fn(),
94
+ showDmsError: vi.fn(),
85
95
  validateRequiredQueryParams: vi.fn(),
86
96
  useDefinedFunctions: () => ({ getFunction: () => undefined }),
87
97
  useDmsRoute: () => ({ path: "/examples/overview", query: {} }),
@@ -101,7 +101,7 @@ beforeEach(async () => {
101
101
  vi.stubGlobal("useDmsRuntimeConfig", () => ({
102
102
  public: { dms: { baseURL: BASE_URL } },
103
103
  }));
104
- vi.stubGlobal("useUserSession", () => ({ loggedIn }));
104
+ vi.stubGlobal("useDmsUserSession", () => ({ loggedIn }));
105
105
  vi.stubGlobal("useSessionRecovery", () => ({
106
106
  refreshSession: async () => true,
107
107
  redirectToAuth: async () => {},
@@ -18,9 +18,6 @@ vi.stubGlobal("useDmsState", <T>(key: string, init?: () => T): Ref<T> => {
18
18
  const { usePageRecordLabel } = await import(
19
19
  "../layers/dms-core/app/composables/page/usePageRecordLabel"
20
20
  );
21
- const legacy = await import(
22
- "../layers/dms-core/app/build/composables/page/usePageRecordLabel"
23
- );
24
21
 
25
22
  beforeEach(() => {
26
23
  sharedState = new Map();
@@ -47,7 +44,3 @@ it("never shows a label on another page, and clears with no value", () => {
47
44
  setLabel(route.path, undefined);
48
45
  expect(label.value).toBeUndefined();
49
46
  });
50
-
51
- it("stays importable from its former internal path", () => {
52
- expect(legacy.usePageRecordLabel).toBe(usePageRecordLabel);
53
- });
@@ -27,6 +27,16 @@ import {
27
27
  } from "../layers/dms-layout/app/composables/layout/usePageFillHeight";
28
28
  import type { PageValidation } from "../layers/dms-layout/app/types/page";
29
29
 
30
+ // The page's data refresh and the visit's navigation badges read the
31
+ // realtime session and Inertia's page, which these tests do not run.
32
+ vi.mock(
33
+ "../layers/dms-ui/app/build/composables/live/usePageDataRefresh",
34
+ () => ({ usePageDataRefresh: () => {} }),
35
+ );
36
+ vi.mock(
37
+ "../layers/dms-layout/app/build/composables/navigation/useVisitNavBadges",
38
+ () => ({ useVisitNavBadges: () => {} }),
39
+ );
30
40
  vi.mock(
31
41
  "../layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder",
32
42
  async () => {
@@ -72,7 +82,7 @@ function installRuntime(): void {
72
82
  PAGE_FILL_HEIGHT_CLASSES,
73
83
  providePageFillHeight,
74
84
  usePageFillHeight,
75
- createError: ({ statusCode }: { statusCode: number }) =>
85
+ createDmsError: ({ statusCode }: { statusCode: number }) =>
76
86
  new HttpError(statusCode),
77
87
  defineDmsPageMeta: vi.fn(),
78
88
  firstAccessiblePagePath: vi.fn(),
@@ -81,7 +91,7 @@ function installRuntime(): void {
81
91
  providePageRealtime: vi.fn(),
82
92
  resolveDmsComponent: () => "div",
83
93
  runCustomValidation,
84
- showError: vi.fn(),
94
+ showDmsError: vi.fn(),
85
95
  validateRequiredQueryParams,
86
96
  useDefinedFunctions: () => ({ getFunction: () => undefined }),
87
97
  useDmsRoute: () => ({ path: PAGE_PATH, query: pageQuery.value }),
@@ -0,0 +1,185 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A relative preset (`last-24h`) in a server render and in the browser that
4
+ * hydrates it, through the runtime's real async data: both resolve the period
5
+ * from the instant the server rendered at, so the browser hydrates the same
6
+ * bounds, key and card data without asking again, whatever time it is when
7
+ * it hydrates. Anywhere else, the period follows the browser's clock.
8
+ */
9
+ import {
10
+ createApp,
11
+ createSSRApp,
12
+ defineComponent,
13
+ h,
14
+ nextTick,
15
+ onBeforeUnmount,
16
+ type App,
17
+ type Component,
18
+ } from "vue";
19
+ import { renderToString } from "vue/server-renderer";
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+ import {
22
+ browserTimeZone,
23
+ setReaderTimeZoneSource,
24
+ } from "../layers/dms-core/app/utils/regional";
25
+
26
+ const engine = await import("#dms/frontend-module");
27
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
28
+ vi.stubGlobal("useDmsFetch", engine.useDmsFetch);
29
+ vi.stubGlobal("useDmsState", engine.useDmsState);
30
+ vi.stubGlobal("useDmsCookie", engine.useDmsCookie);
31
+
32
+ const requests: string[] = [];
33
+ vi.stubGlobal("useAuthFetch", () => ({
34
+ $authFetch: async (url: string) => {
35
+ requests.push(url);
36
+ return {
37
+ value: new URL(url, "http://dms.local").searchParams.get("to"),
38
+ };
39
+ },
40
+ }));
41
+
42
+ const { useApiData } = await import(
43
+ "../layers/dms-core/app/composables/auth/useApiData"
44
+ );
45
+ vi.stubGlobal("useApiData", useApiData);
46
+ const { usePeriod } = await import(
47
+ "../layers/dms-core/app/composables/period/usePeriod"
48
+ );
49
+ const { registerPeriodScope } = await import(
50
+ "../layers/dms-core/app/composables/period/usePeriodScope"
51
+ );
52
+ const { TIME_ZONE_COOKIE, useTimeZoneCookie } = await import(
53
+ "../layers/dms-core/app/composables/period/periodTimeZone"
54
+ );
55
+ setReaderTimeZoneSource(() => useTimeZoneCookie().value);
56
+ const ZONE_COOKIE = `${TIME_ZONE_COOKIE}=${encodeURIComponent(JSON.stringify(browserTimeZone()))}`;
57
+ // jsdom's `document` stays while the server renders: its cookies are the ones
58
+ // read then, as the request's would be.
59
+ document.cookie = ZONE_COOKIE;
60
+ const { useChartFetch } = await import(
61
+ "../layers/dms-ui/app/composables/chart/useChartFetch"
62
+ );
63
+
64
+ const SCOPE = "ops";
65
+ const SERVER_NOW = new Date("2026-10-15T10:00:30.000Z");
66
+ // The browser hydrates a minute and a half later, past a minute boundary.
67
+ const HYDRATION_DELAY_MS = 90_000;
68
+
69
+ const Selector = defineComponent({
70
+ setup() {
71
+ const period = usePeriod({ defaultPreset: "last-24h" });
72
+ onBeforeUnmount(registerPeriodScope(SCOPE, period.state));
73
+ return () => h("i", period.state.value.key);
74
+ },
75
+ });
76
+
77
+ const Card = defineComponent({
78
+ setup() {
79
+ const { data, isLoading } = useChartFetch<{ value: string }>({
80
+ fetchUrl: "/api/errors",
81
+ periodScope: SCOPE,
82
+ });
83
+ return () =>
84
+ h("p", isLoading.value ? "SKELETON" : `to:${data.value?.value}`);
85
+ },
86
+ });
87
+
88
+ const Page: Component = {
89
+ render: () => h("main", [h(Selector), h(Card)]),
90
+ };
91
+
92
+ const mounted: App[] = [];
93
+
94
+ beforeEach(() => {
95
+ requests.length = 0;
96
+ vi.useFakeTimers({ toFake: ["Date"] });
97
+ vi.setSystemTime(SERVER_NOW);
98
+ });
99
+
100
+ afterEach(() => {
101
+ mounted.splice(0).forEach((app) => app.unmount());
102
+ vi.useRealTimers();
103
+ });
104
+
105
+ async function serverRender() {
106
+ const runtime = engine.createDmsFrontendRuntime(
107
+ undefined,
108
+ {},
109
+ true,
110
+ ZONE_COOKIE,
111
+ );
112
+ const app = createSSRApp(Page);
113
+ engine.provideDmsFrontendRuntime(app, runtime);
114
+ engine.setDmsServerRuntimeResolver(() => runtime);
115
+ // What the server sees: no `window`.
116
+ const browserWindow = globalThis.window;
117
+ vi.stubGlobal("window", undefined);
118
+ try {
119
+ const html = await renderToString(app);
120
+ return { html, payload: engine.serializeDmsPayload(runtime) };
121
+ } finally {
122
+ vi.stubGlobal("window", browserWindow);
123
+ }
124
+ }
125
+
126
+ async function hydrate(html: string, payload: unknown) {
127
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
128
+ engine.restoreDmsPayload(
129
+ browser,
130
+ structuredClone(payload) as Parameters<typeof engine.restoreDmsPayload>[1],
131
+ );
132
+ const container = document.createElement("div");
133
+ container.innerHTML = html;
134
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
135
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
136
+ const app = createSSRApp(Page);
137
+ engine.provideDmsFrontendRuntime(app, browser);
138
+ app.mount(container);
139
+ mounted.push(app);
140
+ await nextTick();
141
+ await new Promise((done) => setTimeout(done, 300));
142
+ const mismatches = [...warn.mock.calls, ...error.mock.calls].filter((call) =>
143
+ /hydration/i.test(String(call[0])),
144
+ );
145
+ warn.mockRestore();
146
+ error.mockRestore();
147
+ return { container, mismatches };
148
+ }
149
+
150
+ describe("a relative preset across a server render and its hydration", () => {
151
+ it("renders the card's data with the server's instant", async () => {
152
+ const { html } = await serverRender();
153
+
154
+ expect(html).toContain(`to:${SERVER_NOW.toISOString()}`);
155
+ expect(html).toContain(`|${SERVER_NOW.toISOString()}|`);
156
+ expect(requests).toHaveLength(1);
157
+ });
158
+
159
+ it("hydrates the same bounds later on: no request, no mismatch", async () => {
160
+ const { html, payload } = await serverRender();
161
+ requests.length = 0;
162
+ vi.setSystemTime(SERVER_NOW.getTime() + HYDRATION_DELAY_MS);
163
+
164
+ const { container, mismatches } = await hydrate(html, payload);
165
+
166
+ expect(mismatches).toEqual([]);
167
+ expect(requests).toEqual([]);
168
+ expect(container.innerHTML).toBe(html);
169
+ });
170
+
171
+ it("follows the browser's clock on a client mount", async () => {
172
+ const later = SERVER_NOW.getTime() + HYDRATION_DELAY_MS;
173
+ vi.setSystemTime(later);
174
+ const app = createApp(Selector);
175
+ engine.provideDmsFrontendRuntime(
176
+ app,
177
+ engine.createDmsFrontendRuntime(undefined, {}),
178
+ );
179
+ const root = document.createElement("div");
180
+ app.mount(root);
181
+ mounted.push(app);
182
+
183
+ expect(root.textContent).toContain(`|${new Date(later).toISOString()}|`);
184
+ });
185
+ });
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { createApp, defineComponent, h, ref } from "vue";
9
9
  import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import { useRealDataEngine } from "./fixtures/dataEngine";
10
11
  import { createPageLiveController } from "../layers/dms-ui/app/build/composables/live/pageLiveController";
11
12
  import { setCurrentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
12
13
  import { usePageLive } from "../layers/dms-ui/app/composables/live/usePageLive";
@@ -57,6 +58,8 @@ function mountPageWithChart(): UsePeriodReturn {
57
58
  return period!;
58
59
  }
59
60
 
61
+ beforeEach(() => useRealDataEngine());
62
+
60
63
  beforeEach(() => {
61
64
  vi.useFakeTimers();
62
65
  requests.length = 0;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * A server render resolves periods and writes dates in the time zone of the
3
+ * browser it renders for (its `dms-time-zone` cookie), whatever the server's
4
+ * own: the same bounds and the same text as the browser's, so hydration
5
+ * matches. Without the cookie, or in the server's own zone, nothing changes.
6
+ */
7
+ import { afterEach, describe, expect, it } from "vitest";
8
+ import {
9
+ browserTimeZone,
10
+ regionalDateTimeFormat,
11
+ regionalDayKey,
12
+ setReaderTimeZoneSource,
13
+ } from "../layers/dms-core/app/utils/regional";
14
+ import {
15
+ localClock,
16
+ zonedClock,
17
+ } from "../layers/dms-core/app/composables/period/periodClock";
18
+ import { resolvePresetRange } from "../layers/dms-core/app/composables/period/presetResolvers";
19
+ import { resolveComparisonRange } from "../layers/dms-core/app/composables/period/compareResolvers";
20
+ import {
21
+ formatPeriodDate,
22
+ parsePeriodDate,
23
+ } from "../layers/dms-ui/app/build/composables/period/periodQuery";
24
+ import { formatPeriodRange } from "../layers/dms-ui/app/build/composables/period/periodDisplay";
25
+
26
+ // A zone far from any CI's, and one with a daylight saving change.
27
+ const READER_ZONE =
28
+ browserTimeZone() === "Pacific/Kiritimati"
29
+ ? "Etc/GMT+12"
30
+ : "Pacific/Kiritimati";
31
+ const NEW_YORK = "America/New_York";
32
+ const NOW = new Date("2026-10-14T21:30:00.000Z");
33
+ const FALLBACK = { from: NOW, to: NOW };
34
+
35
+ function readFor(zone: string | null): void {
36
+ setReaderTimeZoneSource(() => zone);
37
+ }
38
+
39
+ function inZone(zone: string, date: Date) {
40
+ const { year, month, day, hours, minutes } = zonedClock(zone).wall(date);
41
+ return `${year}-${month + 1}-${day} ${hours}:${minutes}`;
42
+ }
43
+
44
+ afterEach(() => readFor(null));
45
+
46
+ describe("the clock a period is resolved in", () => {
47
+ it("reads and builds wall times in a zone, across a daylight saving change", () => {
48
+ const clock = zonedClock(NEW_YORK);
49
+ // 8 March 2026: New York moves from UTC-5 to UTC-4 at 2:00.
50
+ const midnight = clock.instant({
51
+ year: 2026,
52
+ month: 2,
53
+ day: 8,
54
+ hours: 0,
55
+ minutes: 0,
56
+ seconds: 0,
57
+ milliseconds: 0,
58
+ });
59
+ expect(midnight.toISOString()).toBe("2026-03-08T05:00:00.000Z");
60
+ const evening = clock.instant({ ...clock.wall(midnight), hours: 23 });
61
+ expect(evening.toISOString()).toBe("2026-03-09T03:00:00.000Z");
62
+ // Day 0 rolls back to the last day of the month before, as Date's does.
63
+ const rolled = clock.wall(
64
+ clock.instant({ ...clock.wall(midnight), month: 3, day: 0 }),
65
+ );
66
+ expect([rolled.month, rolled.day]).toEqual([2, 31]);
67
+ });
68
+
69
+ it("matches Date's own calendar in the zone the code runs in", () => {
70
+ const local = zonedClock(browserTimeZone());
71
+ expect(local.wall(NOW)).toEqual(localClock.wall(NOW));
72
+ const wall = { ...localClock.wall(NOW), day: 40, hours: 0 };
73
+ expect(local.instant(wall).getTime()).toBe(
74
+ localClock.instant(wall).getTime(),
75
+ );
76
+ });
77
+ });
78
+
79
+ describe("a server render for a browser in another zone", () => {
80
+ it("resolves presets and comparisons on the browser's calendar days", () => {
81
+ readFor(READER_ZONE);
82
+ const range = resolvePresetRange("this-month", NOW, FALLBACK)!;
83
+ const reader = zonedClock(READER_ZONE);
84
+ expect(inZone(READER_ZONE, range.from)).toMatch(/-1 0:0$/);
85
+ expect(reader.wall(range.to).hours).toBe(23);
86
+ expect(reader.wall(range.to).month).toBe(reader.wall(range.from).month);
87
+ const previous = resolveComparisonRange("previous-year", range, null)!;
88
+ expect(reader.wall(previous.from).year).toBe(
89
+ reader.wall(range.from).year - 1,
90
+ );
91
+ expect(inZone(READER_ZONE, previous.from)).toMatch(/-1 0:0$/);
92
+ });
93
+
94
+ it("reads and writes the URL's days as the browser's", () => {
95
+ readFor(READER_ZONE);
96
+ const from = parsePeriodDate("2026-10-01", "start")!;
97
+ expect(inZone(READER_ZONE, from)).toBe("2026-10-1 0:0");
98
+ expect(formatPeriodDate(from, "start")).toBe("2026-10-01");
99
+ expect(parsePeriodDate("2026-02-30", "start")).toBeNull();
100
+ });
101
+
102
+ it("writes the period's label and other dates in the browser's zone", () => {
103
+ readFor(READER_ZONE);
104
+ const range = resolvePresetRange("this-month", NOW, FALLBACK)!;
105
+ const expected = new Intl.DateTimeFormat("en", {
106
+ month: "short",
107
+ day: "numeric",
108
+ timeZone: READER_ZONE,
109
+ }).format(range.from);
110
+ expect(formatPeriodRange(range, "en")).toContain(expected);
111
+ expect(
112
+ regionalDateTimeFormat("en", { dateStyle: "medium" }).format(NOW),
113
+ ).toBe(
114
+ new Intl.DateTimeFormat("en", {
115
+ dateStyle: "medium",
116
+ timeZone: READER_ZONE,
117
+ }).format(NOW),
118
+ );
119
+ });
120
+
121
+ it("files dates under the browser's calendar days", () => {
122
+ readFor(READER_ZONE);
123
+ const { year, month, day } = zonedClock(READER_ZONE).wall(NOW);
124
+ const pad = (value: number) => String(value).padStart(2, "0");
125
+ expect(regionalDayKey(NOW)).toBe(`${year}-${pad(month + 1)}-${pad(day)}`);
126
+ });
127
+
128
+ it("changes nothing without the browser's zone, or in the server's own", () => {
129
+ for (const zone of [null, browserTimeZone()]) {
130
+ readFor(zone);
131
+ const range = resolvePresetRange("this-month", NOW, FALLBACK)!;
132
+ expect(range.from).toEqual(
133
+ new Date(NOW.getFullYear(), NOW.getMonth(), 1),
134
+ );
135
+ }
136
+ });
137
+ });
@@ -19,6 +19,7 @@ import {
19
19
  } from "vue";
20
20
  import { renderToString } from "vue/server-renderer";
21
21
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
+ import { useRealDataEngine } from "./fixtures/dataEngine";
22
23
  import type { PeriodState } from "../layers/dms-core/app/composables/period/types";
23
24
 
24
25
  const requests: string[] = [];
@@ -143,7 +144,13 @@ async function ssrThenHydrate(
143
144
  const container = document.createElement("div");
144
145
  container.innerHTML = html;
145
146
  document.body.appendChild(container);
147
+ // The browser starts from what the page carried, as it does after a server
148
+ // render: its first mount takes the data over instead of reading it again.
149
+ const payload = structuredClone(engine.serializeDmsPayload(renderRuntime));
150
+ const browser = engine.createDmsFrontendRuntime(undefined, {});
151
+ engine.restoreDmsPayload(browser, payload);
146
152
  const app = withComponents(createSSRApp(root as never), components);
153
+ engine.provideDmsFrontendRuntime(app, browser);
147
154
  app.mount(container);
148
155
  mounted.push(app);
149
156
  const deadline = Date.now() + SETTLE_MS;
@@ -163,6 +170,13 @@ function withComponents<
163
170
  return app;
164
171
  }
165
172
 
173
+ const engine = await import("#dms/frontend-module");
174
+ let renderRuntime: ReturnType<typeof useRealDataEngine>;
175
+
176
+ beforeEach(() => {
177
+ renderRuntime = useRealDataEngine();
178
+ });
179
+
166
180
  beforeEach(() => {
167
181
  requests.length = 0;
168
182
  });
@@ -238,6 +252,8 @@ describe("period-scoped cards on first paint", () => {
238
252
  }
239
253
  });
240
254
 
255
+ // Its period unknown to the server (no selector registered it before the
256
+ // card set up), a card renders its placeholder, never an empty value.
241
257
  it("shows a loading placeholder server-side rather than an empty value", async () => {
242
258
  const app = createSSRApp({
243
259
  render: () =>
@@ -246,7 +262,7 @@ describe("period-scoped cards on first paint", () => {
246
262
  componentId: "card",
247
263
  pageId: "page",
248
264
  fetchUrl: "/api/metrics/kpi/revenue",
249
- periodScope: SCOPE,
265
+ periodScope: "unregistered-scope",
250
266
  }),
251
267
  });
252
268
  withComponents(app, {
@@ -0,0 +1,195 @@
1
+ /**
2
+ * A server render of a dashboard, through the runtime's real async data: a
3
+ * card bound to a period scope renders its data when the server resolves the
4
+ * period as the browser will (in the browser's time zone, read in its cookie;
5
+ * a selector set up before the card), and its placeholder
6
+ * otherwise, requesting nothing. Each request has its scopes.
7
+ */
8
+ import { createSSRApp, defineComponent, h, ref, type Component } from "vue";
9
+ import { renderToString } from "vue/server-renderer";
10
+ import { beforeEach, describe, expect, it, vi } from "vitest";
11
+ import type { PeriodPreset } from "../layers/dms-core/app/composables/period/types";
12
+ import {
13
+ browserTimeZone,
14
+ setReaderTimeZoneSource,
15
+ } from "../layers/dms-core/app/utils/regional";
16
+
17
+ const engine = await import("#dms/frontend-module");
18
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
19
+ vi.stubGlobal("useDmsFetch", engine.useDmsFetch);
20
+ vi.stubGlobal("useDmsState", engine.useDmsState);
21
+ vi.stubGlobal("useDmsCookie", engine.useDmsCookie);
22
+
23
+ const requests: string[] = [];
24
+ vi.stubGlobal("useAuthFetch", () => ({
25
+ $authFetch: async (url: string) => {
26
+ requests.push(url);
27
+ await new Promise((done) => setTimeout(done, 5));
28
+ return {
29
+ value: new URL(url, "http://dms.local").searchParams.get("preset"),
30
+ };
31
+ },
32
+ }));
33
+
34
+ const { useApiData } = await import(
35
+ "../layers/dms-core/app/composables/auth/useApiData"
36
+ );
37
+ vi.stubGlobal("useApiData", useApiData);
38
+ const { usePeriod } = await import(
39
+ "../layers/dms-core/app/composables/period/usePeriod"
40
+ );
41
+ const { registerPeriodScope } = await import(
42
+ "../layers/dms-core/app/composables/period/usePeriodScope"
43
+ );
44
+ const { TIME_ZONE_COOKIE, useTimeZoneCookie } = await import(
45
+ "../layers/dms-core/app/composables/period/periodTimeZone"
46
+ );
47
+ const { zonedClock } = await import(
48
+ "../layers/dms-core/app/composables/period/periodClock"
49
+ );
50
+ // What the regional-preferences plugin sets up.
51
+ setReaderTimeZoneSource(() => useTimeZoneCookie().value);
52
+ const { useChartFetch } = await import(
53
+ "../layers/dms-ui/app/composables/chart/useChartFetch"
54
+ );
55
+
56
+ const SCOPE = "dashboard";
57
+
58
+ function selector(preset: PeriodPreset): Component {
59
+ return defineComponent({
60
+ setup() {
61
+ const period = usePeriod({ defaultPreset: preset });
62
+ registerPeriodScope(SCOPE, period.state);
63
+ return () => h("i");
64
+ },
65
+ });
66
+ }
67
+
68
+ const Card = defineComponent({
69
+ setup() {
70
+ const { data, isLoading } = useChartFetch<{ value: string }>({
71
+ fetchUrl: "/api/kpi",
72
+ periodScope: SCOPE,
73
+ });
74
+ return () =>
75
+ h("p", isLoading.value ? "SKELETON" : `value:${data.value?.value}`);
76
+ },
77
+ });
78
+
79
+ function zoneCookie(zone: string): string {
80
+ return `${TIME_ZONE_COOKIE}=${encodeURIComponent(JSON.stringify(zone))}`;
81
+ }
82
+
83
+ async function serverRender(children: Component[], cookies?: string) {
84
+ const runtime = engine.createDmsFrontendRuntime(undefined, {}, true, cookies);
85
+ const app = createSSRApp({
86
+ render: () =>
87
+ h(
88
+ "main",
89
+ children.map((c) => h(c)),
90
+ ),
91
+ });
92
+ engine.provideDmsFrontendRuntime(app, runtime);
93
+ engine.setDmsServerRuntimeResolver(() => runtime);
94
+ const html = await renderToString(app);
95
+ return { html, payload: engine.serializeDmsPayload(runtime) };
96
+ }
97
+
98
+ beforeEach(() => {
99
+ requests.length = 0;
100
+ });
101
+
102
+ describe("a period-scoped card in a server render", () => {
103
+ it("renders its data with the period the browser resolves, and hands it over", async () => {
104
+ const { html, payload } = await serverRender(
105
+ [selector("this-month"), Card],
106
+ zoneCookie(browserTimeZone()),
107
+ );
108
+
109
+ expect(html).toContain("value:this-month");
110
+ expect(requests).toHaveLength(1);
111
+ expect(requests[0]).toMatch(
112
+ /^\/api\/kpi\?from=.*&preset=this-month&comparison=none$/,
113
+ );
114
+ expect(Object.values(payload.data)).toEqual([{ value: "this-month" }]);
115
+ });
116
+
117
+ it("leaves it to the browser without the browser's time zone", async () => {
118
+ const { html } = await serverRender([selector("this-month"), Card]);
119
+
120
+ expect(html).toContain("SKELETON");
121
+ expect(requests).toEqual([]);
122
+ });
123
+
124
+ it("resolves the period in the browser's zone when it is not the server's", async () => {
125
+ const elsewhere =
126
+ browserTimeZone() === "Pacific/Kiritimati"
127
+ ? "Etc/GMT+12"
128
+ : "Pacific/Kiritimati";
129
+ const { html } = await serverRender(
130
+ [selector("this-month"), Card],
131
+ zoneCookie(elsewhere),
132
+ );
133
+
134
+ expect(html).toContain("value:this-month");
135
+ const query = new URL(requests[0]!, "http://dms.local").searchParams;
136
+ const from = zonedClock(elsewhere).wall(new Date(query.get("from")!));
137
+ const to = zonedClock(elsewhere).wall(new Date(query.get("to")!));
138
+ expect([from.day, from.hours, from.minutes]).toEqual([1, 0, 0]);
139
+ expect([to.hours, to.minutes, to.milliseconds]).toEqual([23, 59, 999]);
140
+ expect(to.month).toBe(from.month);
141
+ });
142
+
143
+ it("renders a relative preset too, from the instant it hands the browser", async () => {
144
+ const { html, payload } = await serverRender(
145
+ [selector("last-24h"), Card],
146
+ zoneCookie(browserTimeZone()),
147
+ );
148
+
149
+ expect(html).toContain("value:last-24h");
150
+ expect(requests).toHaveLength(1);
151
+ const query = new URL(requests[0]!, "http://dms.local").searchParams;
152
+ expect(payload.state["dms-period-render-now"]).toBe(
153
+ new Date(query.get("to")!).getTime(),
154
+ );
155
+ });
156
+
157
+ it("leaves it to the browser when its selector sets up after it", async () => {
158
+ const { html } = await serverRender(
159
+ [Card, selector("this-month")],
160
+ zoneCookie(browserTimeZone()),
161
+ );
162
+
163
+ expect(html).toContain("SKELETON");
164
+ expect(requests).toEqual([]);
165
+ });
166
+
167
+ it("keeps the scopes of concurrent requests apart", async () => {
168
+ const cookie = zoneCookie(browserTimeZone());
169
+ const [month, year] = await Promise.all([
170
+ serverRender([selector("this-month"), Card], cookie),
171
+ serverRender([selector("last-year"), Card], cookie),
172
+ ]);
173
+
174
+ expect(month.html).toContain("value:this-month");
175
+ expect(year.html).toContain("value:last-year");
176
+ });
177
+ });
178
+
179
+ describe("a card without a period in a server render", () => {
180
+ it("renders its data", async () => {
181
+ const Plain = defineComponent({
182
+ setup() {
183
+ const { data } = useChartFetch<{ value: string | null }>({
184
+ fetchUrl: "/api/plain",
185
+ watchSource: () => ref(1).value,
186
+ });
187
+ return () => h("p", `plain:${data.value ? "loaded" : "none"}`);
188
+ },
189
+ });
190
+ const { html } = await serverRender([Plain]);
191
+
192
+ expect(html).toContain("plain:loaded");
193
+ expect(requests).toEqual(["/api/plain"]);
194
+ });
195
+ });