@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
@@ -79,10 +79,7 @@ function stubSiteLayout(moduleRoot: TreeNode, landingPage?: string) {
79
79
  }));
80
80
  }
81
81
 
82
- async function enter(
83
- fullPath: string,
84
- drawnThere = false,
85
- ): Promise<unknown> {
82
+ async function enter(fullPath: string, drawnThere = false): Promise<unknown> {
86
83
  const middleware = (
87
84
  await import("../layers/dms-layout/app/middleware/module-routing.global")
88
85
  ).default as unknown as Middleware;
@@ -98,7 +95,7 @@ beforeEach(() => {
98
95
  vi.stubGlobal("navigateDms", navigateDms);
99
96
  vi.stubGlobal("useHomepage", () => "/dashboard");
100
97
  vi.stubGlobal("useModuleHistory", () => ({ record: vi.fn() }));
101
- vi.stubGlobal("createError", (error: unknown) => error);
98
+ vi.stubGlobal("createDmsError", (error: unknown) => error);
102
99
  });
103
100
 
104
101
  afterEach(() => {
@@ -117,7 +117,7 @@ describe("findModulePageMatch", () => {
117
117
  "/modules/saas/billing/invoices": page("/modules/saas/billing/invoices", {
118
118
  displayName: "Invoices",
119
119
  module: "saas",
120
- category: { displayName: "Billing" },
120
+ categoryId: "saas.billing",
121
121
  }),
122
122
  "/modules/saas/invoice/:id": page("/modules/saas/invoice/:id", {
123
123
  displayName: "Invoice detail",
@@ -130,9 +130,22 @@ describe("findModulePageMatch", () => {
130
130
  }),
131
131
  };
132
132
 
133
+ // Every category once, by full id: a page names its own.
134
+ const layout = {
135
+ pages,
136
+ categories: {
137
+ "saas.billing": {
138
+ id: "billing",
139
+ fullId: "saas.billing",
140
+ fullSlug: "/modules/saas/billing",
141
+ displayName: "Billing",
142
+ },
143
+ },
144
+ };
145
+
133
146
  it("finds an openable page of an installed module and its section", () => {
134
147
  expect(
135
- findModulePageMatch(pages, "invoice", new Set(["saas"]), resolve),
148
+ findModulePageMatch(layout, "invoice", new Set(["saas"]), resolve),
136
149
  ).toEqual({
137
150
  moduleId: "saas",
138
151
  path: "/modules/saas/billing/invoices",
@@ -143,10 +156,10 @@ describe("findModulePageMatch", () => {
143
156
 
144
157
  it("ignores hidden pages, parameterised pages and other modules", () => {
145
158
  expect(
146
- findModulePageMatch(pages, "detail", new Set(["saas"]), resolve),
159
+ findModulePageMatch(layout, "detail", new Set(["saas"]), resolve),
147
160
  ).toBeNull();
148
161
  expect(
149
- findModulePageMatch(pages, "invoices", new Set(["hidden"]), resolve),
162
+ findModulePageMatch(layout, "invoices", new Set(["hidden"]), resolve),
150
163
  ).toBeNull();
151
164
  });
152
165
  });
@@ -0,0 +1,95 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The modules catalog through the runtime's real async data: a server render
4
+ * reads it for a platform owner and hands it to the browser, which does not
5
+ * ask again (its live readouts would no longer match the markup); a member
6
+ * reads nothing; two renders in flight at once each render their own.
7
+ */
8
+ import { computed, defineComponent, h, nextTick, ref, type App } from "vue";
9
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import { hydrate, serverRender, stubApiData } from "./helpers/api-data-runtime";
11
+ import { useModulesListing } from "../layers/dms-layout/app/composables/page/useModulesListing";
12
+
13
+ const ordersModule = { id: "orders", title: "Orders", hasAccess: true };
14
+ const billingModule = { id: "billing", title: "Billing", hasAccess: true };
15
+
16
+ const authFetch = vi.fn();
17
+ const isOwner = ref(true);
18
+ const mounted: App[] = [];
19
+
20
+ const Catalog = defineComponent({
21
+ async setup() {
22
+ const listing = useModulesListing();
23
+ await listing.loadModulesListing();
24
+ return () =>
25
+ h(
26
+ "p",
27
+ listing.loadingError.value?.kind ??
28
+ (listing.modules.value ?? []).map((entry) => entry.id).join(","),
29
+ );
30
+ },
31
+ });
32
+ const Page = defineComponent({
33
+ setup: () => () => h("main", [h(Catalog)]),
34
+ });
35
+
36
+ beforeEach(() => {
37
+ isOwner.value = true;
38
+ vi.stubGlobal("computed", computed);
39
+ vi.stubGlobal("HTTP_FORBIDDEN", 403);
40
+ vi.stubGlobal("useIsOwner", () => computed(() => isOwner.value));
41
+ stubApiData(authFetch);
42
+ });
43
+
44
+ afterEach(() => {
45
+ mounted.splice(0).forEach((app) => app.unmount());
46
+ vi.unstubAllGlobals();
47
+ vi.resetAllMocks();
48
+ });
49
+
50
+ it("hands the listing the server rendered to the browser, which does not ask again", async () => {
51
+ authFetch.mockResolvedValue([ordersModule]);
52
+ const { html, payload } = await serverRender(Page);
53
+ expect(html).toBe("<main><p>orders</p></main>");
54
+ expect(authFetch).toHaveBeenCalledTimes(1);
55
+
56
+ authFetch.mockClear();
57
+ const { app, container } = hydrate(Page, html, payload);
58
+ mounted.push(app);
59
+ await nextTick();
60
+ await new Promise((done) => setTimeout(done, 0));
61
+ expect(authFetch).not.toHaveBeenCalled();
62
+ expect(container.innerHTML).toBe(html);
63
+ });
64
+
65
+ it("reads nothing for a member", async () => {
66
+ isOwner.value = false;
67
+ const { html } = await serverRender(Page);
68
+ expect(html).toBe("<main><p></p></main>");
69
+ expect(authFetch).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it("shows a refused listing as forbidden", async () => {
73
+ authFetch.mockRejectedValue(
74
+ Object.assign(new Error("Forbidden"), { statusCode: 403 }),
75
+ );
76
+ const { html } = await serverRender(Page);
77
+ expect(html).toBe("<main><p>forbidden</p></main>");
78
+ });
79
+
80
+ it("renders each request's own listing when two renders are in flight at once", async () => {
81
+ let answerFirst!: (value: unknown) => void;
82
+ authFetch
83
+ .mockReturnValueOnce(
84
+ new Promise((resolve) => {
85
+ answerFirst = resolve;
86
+ }),
87
+ )
88
+ .mockResolvedValueOnce([billingModule]);
89
+ const first = serverRender(Page);
90
+ const second = serverRender(Page);
91
+ await new Promise((done) => setTimeout(done, 0));
92
+ answerFirst([ordersModule]);
93
+ expect((await first).html).toBe("<main><p>orders</p></main>");
94
+ expect((await second).html).toBe("<main><p>billing</p></main>");
95
+ });
@@ -1,5 +1,8 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { validateAccount } from "../layers/dms-core/app/composables/auth/useMultiAccount";
2
+ import {
3
+ refreshSessionSurfaces,
4
+ validateAccount,
5
+ } from "../layers/dms-core/app/composables/auth/useMultiAccount";
3
6
  import type { StoredAccount } from "../layers/dms-core/app/types/auth";
4
7
 
5
8
  function makeAccount(overrides: Partial<StoredAccount> = {}): StoredAccount {
@@ -97,3 +100,22 @@ describe("validateAccount", () => {
97
100
  expect(account).not.toHaveProperty("refreshToken");
98
101
  });
99
102
  });
103
+
104
+ describe("refreshSessionSurfaces", () => {
105
+ afterEach(() => vi.unstubAllGlobals());
106
+
107
+ it("reads the session again, then reloads the site layout through Inertia", async () => {
108
+ const calls: string[] = [];
109
+ vi.stubGlobal("useSiteLayout", () => ({
110
+ resync: vi.fn(async () => calls.push("inertia reload")),
111
+ refresh: vi.fn(async () => calls.push("site layout fetch")),
112
+ }));
113
+ vi.stubGlobal("usePermissions", () => ({
114
+ clear: () => calls.push("permissions cleared"),
115
+ }));
116
+
117
+ await refreshSessionSurfaces(async () => calls.push("session"));
118
+
119
+ expect(calls).toEqual(["session", "permissions cleared", "inertia reload"]);
120
+ });
121
+ });
@@ -3,6 +3,7 @@ import {
3
3
  computed,
4
4
  createApp,
5
5
  defineComponent,
6
+ effectScope,
6
7
  h,
7
8
  onMounted,
8
9
  ref,
@@ -51,8 +52,6 @@ beforeEach(() => {
51
52
  vi.stubGlobal("useTranslation", () => ({
52
53
  processI18n: (value: string) => value,
53
54
  }));
54
- vi.stubGlobal("usePrefetch", () => ({ prefetchPageLayout: vi.fn() }));
55
- vi.stubGlobal("stripQueryAndHash", (path: string) => path);
56
55
  vi.stubGlobal("MENU_STATUS_TEXT_CLASSES", MENU_STATUS_TEXT_CLASSES);
57
56
  host = document.createElement("div");
58
57
  document.body.append(host);
@@ -86,6 +85,43 @@ describe("useNavBadges", () => {
86
85
  });
87
86
  });
88
87
 
88
+ describe("the counts a page visit carries", () => {
89
+ it("draws each one, over a static badge", () => {
90
+ const { navBadgeOf, applyCountedNavBadges } = useNavBadges();
91
+ applyCountedNavBadges({
92
+ "settings.user.notifications": { badge: "4", tone: "error" },
93
+ });
94
+ expect(
95
+ navBadgeOf({ fullId: "settings.user.notifications", badge: "new" }),
96
+ ).toEqual({ label: "4", tone: "error" });
97
+ });
98
+
99
+ it("drops the badge of an entry the next visit no longer counts", () => {
100
+ const { navBadgeOf, applyCountedNavBadges } = useNavBadges();
101
+ applyCountedNavBadges({
102
+ "settings.user.notifications": { badge: "4" },
103
+ "settings.workspace.invites": { badge: "2" },
104
+ });
105
+ applyCountedNavBadges({ "settings.workspace.invites": { badge: "1" } });
106
+
107
+ expect(
108
+ navBadgeOf({ fullId: "settings.user.notifications" }),
109
+ ).toBeUndefined();
110
+ expect(navBadgeOf({ fullId: "settings.workspace.invites" })).toEqual({
111
+ label: "1",
112
+ });
113
+ });
114
+
115
+ it("leaves the badges alone when a visit carries none, such as a frontend page", () => {
116
+ const { navBadgeOf, applyCountedNavBadges } = useNavBadges();
117
+ applyCountedNavBadges({ "settings.user.notifications": { badge: "4" } });
118
+ applyCountedNavBadges(undefined);
119
+ expect(navBadgeOf({ fullId: "settings.user.notifications" })).toEqual({
120
+ label: "4",
121
+ });
122
+ });
123
+ });
124
+
89
125
  describe("NavigationMenu badges", () => {
90
126
  // Renders each entry's badge the way UNavigationMenu hands it over.
91
127
  const MenuStub = defineComponent({
@@ -102,6 +138,7 @@ describe("NavigationMenu badges", () => {
102
138
  "data-badge": item.badge?.label,
103
139
  "data-color": item.badge?.color,
104
140
  "data-variant": item.badge?.variant,
141
+ "data-prefetch": String(item.prefetch),
105
142
  });
106
143
  return () => h("nav", props.items.flat().map(renderItem));
107
144
  },
@@ -150,6 +187,19 @@ describe("NavigationMenu badges", () => {
150
187
  expect(badgeOf("settings.user.notifications").color).toBe("primary");
151
188
  });
152
189
 
190
+ it("lets Inertia prefetch each linked page on hover, with no fetch of its own", () => {
191
+ const fetch = vi.fn();
192
+ vi.stubGlobal("fetch", fetch);
193
+ vi.stubGlobal("$fetch", fetch);
194
+ mount();
195
+ for (const item of ITEMS)
196
+ expect(badgeOf(item.fullId as string).prefetch).toBe("true");
197
+ host
198
+ .querySelector("nav")!
199
+ .dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
200
+ expect(fetch).not.toHaveBeenCalled();
201
+ });
202
+
153
203
  it("follows the live count and tone, and hides the badge at zero", async () => {
154
204
  mount();
155
205
  const { setNavBadge } = useNavBadges();
@@ -259,13 +309,26 @@ describe("Security page badge", () => {
259
309
  const { useSecurityOverview } = await import(
260
310
  "../layers/dms-layout/app/build/composables/settings/security/useSecurityOverview"
261
311
  );
312
+ const { engine, browserRuntime } = await import(
313
+ "./helpers/api-data-runtime"
314
+ );
315
+ const { useApiData } = await import(
316
+ "../layers/dms-core/app/composables/auth/useApiData"
317
+ );
318
+ vi.stubGlobal("useDmsAsyncData", engine.useDmsAsyncData);
319
+ vi.stubGlobal("useDmsFetch", engine.useDmsFetch);
320
+ vi.stubGlobal("useApiData", useApiData);
262
321
  vi.stubGlobal("useAuthFetch", () => ({
263
322
  $authFetch: vi.fn(async () => ({
264
323
  attention: ["backup_codes_low", "backup_codes_unsaved"],
265
324
  attentionTone: "error",
266
325
  })),
267
326
  }));
268
- await useSecurityOverview().refresh();
327
+ browserRuntime();
328
+ const scope = effectScope();
329
+ const overview = scope.run(() => useSecurityOverview())!;
330
+ await overview.refresh();
331
+ scope.stop();
269
332
  expect(liveBadges()?.value["settings.user.security"]).toEqual({
270
333
  label: "2",
271
334
  tone: "error",
@@ -0,0 +1,223 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The bell's counts after a change of the feed: a write (mark read or
4
+ * unread, delete) answers them and its realtime event carries them, so the
5
+ * tab that wrote and every other tab of the user take them as they are. The
6
+ * write's own event, which comes before or after its answer, changes
7
+ * nothing and reads nothing. Through the real composable, its stream plugin
8
+ * and the runtime's state, over a fake feed.
9
+ */
10
+ import { defineComponent, h, nextTick, ref, type App } from "vue";
11
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
12
+ import { mountInBrowser, stubApiData } from "./helpers/api-data-runtime";
13
+
14
+ type Tone = "neutral" | "primary" | "warning" | "error";
15
+ interface Row {
16
+ _id: string;
17
+ isRead: boolean;
18
+ tone: Tone;
19
+ }
20
+ interface Counts {
21
+ all: number;
22
+ unread: number;
23
+ unreadTone?: Tone;
24
+ }
25
+ type StreamEvent = { type: string; payload?: Record<string, unknown> };
26
+
27
+ const TONE_ORDER: Tone[] = ["error", "warning", "primary", "neutral"];
28
+ const feed: Row[] = [];
29
+
30
+ /** The whole feed's counts, as `countFeedWithUnreadTone` gives them. */
31
+ function countsOf(): Counts {
32
+ const unread = feed.filter((row) => !row.isRead);
33
+ const tone = TONE_ORDER.find((t) => unread.some((row) => row.tone === t));
34
+ return tone
35
+ ? { all: feed.length, unread: unread.length, unreadTone: tone }
36
+ : { all: feed.length, unread: unread.length };
37
+ }
38
+
39
+ const requests: string[] = [];
40
+ // A write's answer can be held, to deliver its echo first.
41
+ let holdWrites: Promise<void> | undefined;
42
+ let failWrites = false;
43
+ const authFetch = vi.fn(async (url: string, options?: { method?: string }) => {
44
+ const method = options?.method ?? "GET";
45
+ requests.push(`${method} ${url}`);
46
+ if (method === "GET" && url.endsWith("/counts")) return countsOf();
47
+ if (failWrites) throw new Error("refused");
48
+ const id = url.split("/").pop();
49
+ const row = feed.find((entry) => entry._id === id);
50
+ if (url.includes("/mark-read/")) row!.isRead = true;
51
+ else if (url.includes("/mark-unread/")) row!.isRead = false;
52
+ else if (url.includes("/delete/")) feed.splice(feed.indexOf(row!), 1);
53
+ const counts = countsOf();
54
+ await holdWrites;
55
+ return { success: true, counts };
56
+ });
57
+
58
+ let streamHandler: ((event: unknown) => void) | undefined;
59
+ stubApiData(authFetch as never);
60
+ vi.stubGlobal("useComponentEvent", () => ({ sendComponentEvent: vi.fn() }));
61
+ vi.stubGlobal("useDmsUserSession", () => ({
62
+ user: ref({ _id: "u1" }),
63
+ loggedIn: ref(true),
64
+ }));
65
+ vi.stubGlobal("useUserRealtime", () => ({
66
+ subscribe: (_topic: string, handler: (event: unknown) => void) => {
67
+ streamHandler = handler;
68
+ return () => {
69
+ streamHandler = undefined;
70
+ };
71
+ },
72
+ }));
73
+ vi.stubGlobal("defineDmsPlugin", (setup: () => void) => setup);
74
+ vi.stubGlobal("watch", (await import("vue")).watch);
75
+ const { useNotifications } = await import(
76
+ "../layers/dms-layout/app/composables/notification/useNotifications"
77
+ );
78
+ vi.stubGlobal("useNotifications", useNotifications);
79
+ const { default: notificationStream } = await import(
80
+ "../layers/dms-layout/app/plugins/notification-stream.client"
81
+ );
82
+
83
+ const mounted: App[] = [];
84
+ let bell: ReturnType<typeof useNotifications>;
85
+
86
+ /** A tab: the bell's state and the stream plugin, on one runtime. */
87
+ function openTab(): HTMLElement {
88
+ const Tab = defineComponent({
89
+ setup() {
90
+ bell = useNotifications();
91
+ (notificationStream as unknown as () => void)();
92
+ return () =>
93
+ h("span", `${bell.unreadCount.value}:${bell.unreadTone.value ?? "-"}`);
94
+ },
95
+ });
96
+ const { app, container } = mountInBrowser(Tab);
97
+ mounted.push(app);
98
+ return container;
99
+ }
100
+
101
+ /** The server's event for a read state change, with the counts after it. */
102
+ const echo = (type: string, ids: string[]): StreamEvent => ({
103
+ type,
104
+ payload: { ids, counts: countsOf() },
105
+ });
106
+
107
+ async function settle(): Promise<void> {
108
+ await nextTick();
109
+ await new Promise((done) => setTimeout(done, 0));
110
+ }
111
+
112
+ beforeEach(async () => {
113
+ feed.splice(
114
+ 0,
115
+ feed.length,
116
+ { _id: "risk", isRead: false, tone: "error" },
117
+ { _id: "check", isRead: false, tone: "warning" },
118
+ { _id: "old", isRead: true, tone: "neutral" },
119
+ );
120
+ requests.length = 0;
121
+ holdWrites = undefined;
122
+ failWrites = false;
123
+ const tab = openTab();
124
+ await bell.fetchBellCounts();
125
+ await settle();
126
+ expect(tab.textContent).toBe("2:error");
127
+ requests.length = 0;
128
+ });
129
+
130
+ afterEach(() => {
131
+ mounted.splice(0).forEach((app) => app.unmount());
132
+ });
133
+
134
+ it("takes the counts a write answers, and reads nothing for its echo after the answer", async () => {
135
+ await bell.markAsRead("risk");
136
+ await settle();
137
+ expect(bell.unreadCount.value).toBe(1);
138
+ // The next most important unread one gives the tone: only the server knows it.
139
+ expect(bell.unreadTone.value).toBe("warning");
140
+
141
+ streamHandler!(echo("notification:read", ["risk"]));
142
+ await settle();
143
+ expect(requests).toEqual([
144
+ "PUT /dms/pages/settings/user/notifications/mark-read/risk",
145
+ ]);
146
+ expect(bell.unreadCount.value).toBe(1);
147
+
148
+ await bell.markAsUnread("risk");
149
+ await bell.deleteNotification("check");
150
+ await settle();
151
+ expect(requests).toEqual([
152
+ "PUT /dms/pages/settings/user/notifications/mark-read/risk",
153
+ "PUT /dms/pages/settings/user/notifications/mark-unread/risk",
154
+ "DELETE /dms/pages/settings/user/notifications/delete/check",
155
+ ]);
156
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([1, "error"]);
157
+ });
158
+
159
+ it("reads nothing when the write's echo comes before its answer", async () => {
160
+ let release!: () => void;
161
+ holdWrites = new Promise((done) => {
162
+ release = done;
163
+ });
164
+ const writing = bell.markAsRead("check");
165
+ await settle();
166
+ streamHandler!(echo("notification:read", ["check"]));
167
+ await settle();
168
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([1, "error"]);
169
+ release();
170
+ await writing;
171
+ await settle();
172
+ expect(requests).toEqual([
173
+ "PUT /dms/pages/settings/user/notifications/mark-read/check",
174
+ ]);
175
+ expect(bell.unreadCount.value).toBe(1);
176
+ });
177
+
178
+ it("takes another tab's changes from their events, without reading", async () => {
179
+ feed[0]!.isRead = true;
180
+ streamHandler!(echo("notification:read", ["risk"]));
181
+ await settle();
182
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([
183
+ 1,
184
+ "warning",
185
+ ]);
186
+
187
+ feed[0]!.isRead = false;
188
+ streamHandler!(echo("notification:unread", ["risk"]));
189
+ await settle();
190
+ expect(bell.unreadTone.value).toBe("error");
191
+
192
+ for (const row of feed) row.isRead = true;
193
+ streamHandler!({
194
+ type: "notification:all-read",
195
+ payload: { counts: countsOf() },
196
+ });
197
+ await settle();
198
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([
199
+ 0,
200
+ undefined,
201
+ ]);
202
+ expect(requests).toEqual([]);
203
+ });
204
+
205
+ it("reads the counts when an event carries none", async () => {
206
+ feed[0]!.isRead = true;
207
+ streamHandler!({ type: "notification:read", payload: { ids: ["risk"] } });
208
+ await settle();
209
+ expect(requests).toEqual([
210
+ "GET /dms/pages/settings/user/notifications/counts",
211
+ ]);
212
+ expect(bell.unreadCount.value).toBe(1);
213
+ });
214
+
215
+ it("keeps the counts when a write fails", async () => {
216
+ failWrites = true;
217
+ await expect(bell.markAsRead("risk")).rejects.toThrow("refused");
218
+ await settle();
219
+ expect(requests).toEqual([
220
+ "PUT /dms/pages/settings/user/notifications/mark-read/risk",
221
+ ]);
222
+ expect([bell.unreadCount.value, bell.unreadTone.value]).toEqual([2, "error"]);
223
+ });
@@ -0,0 +1,88 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The header bell's counts as the dashboard frame reads them with the page,
4
+ * through the runtime's real async data: a server render shows the badge,
5
+ * and the browser's bell, and whatever else shows the counts, start from the
6
+ * same counts without asking again. A signed-out visit reads nothing.
7
+ */
8
+ import { defineComponent, h, nextTick, ref, type App } from "vue";
9
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
10
+ import {
11
+ hydrate,
12
+ mountInBrowser,
13
+ serverRender,
14
+ stubApiData,
15
+ } from "./helpers/api-data-runtime";
16
+
17
+ const COUNTS = { all: 5, unread: 3, unreadTone: "warning" };
18
+ const authFetch = vi.fn(async () => structuredClone(COUNTS));
19
+ const loggedIn = ref(true);
20
+ const mounted: App[] = [];
21
+
22
+ stubApiData(authFetch as never);
23
+ vi.stubGlobal("useDmsUserSession", () => ({ loggedIn }));
24
+ vi.stubGlobal("useComponentEvent", () => ({ sendComponentEvent: vi.fn() }));
25
+ const { useNotifications, useRenderedBellCounts } = await import(
26
+ "../layers/dms-layout/app/composables/notification/useNotifications"
27
+ );
28
+ vi.stubGlobal("useNotifications", useNotifications);
29
+
30
+ /** The bell's badge, or the profile's summary: the shared counts. */
31
+ const Counts = defineComponent({
32
+ setup() {
33
+ const { unreadCount, unreadTone, areCountsLoaded } = useNotifications();
34
+ return () =>
35
+ h(
36
+ "span",
37
+ areCountsLoaded.value
38
+ ? `${unreadCount.value}:${unreadTone.value}`
39
+ : "unknown",
40
+ );
41
+ },
42
+ });
43
+ // As the dashboard frame: in the persistent layout, outside the page's
44
+ // <Suspense>, around the header and the page.
45
+ const Frame = defineComponent({
46
+ setup() {
47
+ useRenderedBellCounts();
48
+ return () => h("div", [h(Counts), h(Counts)]);
49
+ },
50
+ });
51
+
52
+ beforeEach(() => {
53
+ loggedIn.value = true;
54
+ authFetch.mockClear();
55
+ });
56
+
57
+ afterEach(() => {
58
+ mounted.splice(0).forEach((app) => app.unmount());
59
+ vi.unstubAllEnvs();
60
+ });
61
+
62
+ it("renders the bell's counts on the server, and the browser starts from them without asking", async () => {
63
+ vi.stubEnv("SSR", true);
64
+ const { html, payload } = await serverRender(Frame);
65
+ vi.stubEnv("SSR", false);
66
+ expect(html).toBe("<div><span>3:warning</span><span>3:warning</span></div>");
67
+ expect(authFetch).toHaveBeenCalledTimes(1);
68
+
69
+ authFetch.mockClear();
70
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
71
+ const { app, container } = hydrate(Frame, html, payload);
72
+ mounted.push(app);
73
+ await nextTick();
74
+ await new Promise((done) => setTimeout(done, 0));
75
+ expect(authFetch).not.toHaveBeenCalled();
76
+ expect(container.innerHTML).toBe(html);
77
+ expect(warn.mock.calls.flat().join(" ")).not.toMatch(/hydration/i);
78
+ warn.mockRestore();
79
+ });
80
+
81
+ it("reads nothing for a signed-out visit", async () => {
82
+ loggedIn.value = false;
83
+ const { app, container } = mountInBrowser(Frame);
84
+ mounted.push(app);
85
+ await nextTick();
86
+ expect(authFetch).not.toHaveBeenCalled();
87
+ expect(container.textContent).toBe("unknownunknown");
88
+ });