@antelopejs/dms 0.4.5 → 0.4.7

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 (178) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/hooks/invite-extensions.js +2 -2
  3. package/dist/hooks/invite-extensions.js.map +1 -1
  4. package/dist/implementations/dms/layout-banners.d.ts +18 -0
  5. package/dist/implementations/dms/layout-banners.js +60 -0
  6. package/dist/implementations/dms/layout-banners.js.map +1 -0
  7. package/dist/implementations/dms/page.d.ts +2 -0
  8. package/dist/implementations/dms/page.js +82 -27
  9. package/dist/implementations/dms/page.js.map +1 -1
  10. package/dist/implementations/dms/quick-actions.d.ts +5 -0
  11. package/dist/implementations/dms/quick-actions.js.map +1 -1
  12. package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
  13. package/dist/implementations/dms/resolver-timeout.js +13 -0
  14. package/dist/implementations/dms/resolver-timeout.js.map +1 -0
  15. package/dist/implementations/dms/saas-mode.d.ts +8 -0
  16. package/dist/implementations/dms/saas-mode.js +20 -0
  17. package/dist/implementations/dms/saas-mode.js.map +1 -0
  18. package/dist/implementations/dms-auth/index.d.ts +2 -1
  19. package/dist/implementations/dms-auth/index.js +19 -8
  20. package/dist/implementations/dms-auth/index.js.map +1 -1
  21. package/dist/implementations/dms-html-render/service-jwt.js +2 -1
  22. package/dist/implementations/dms-html-render/service-jwt.js.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/pages/settings/users/index.d.ts +1 -0
  26. package/dist/pages/settings/users/index.js +1 -0
  27. package/dist/pages/settings/users/index.js.map +1 -1
  28. package/dist/pages/settings/users/invite-extension-routes.d.ts +4 -0
  29. package/dist/pages/settings/users/invite-extension-routes.js +57 -0
  30. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -0
  31. package/dist/pages/settings/users/invites.d.ts +2 -1
  32. package/dist/pages/settings/users/invites.js +17 -5
  33. package/dist/pages/settings/users/invites.js.map +1 -1
  34. package/dist/pages/settings/users/members.d.ts +4 -1
  35. package/dist/pages/settings/users/members.js +10 -6
  36. package/dist/pages/settings/users/members.js.map +1 -1
  37. package/dist/pages/settings/users/notifications.js +1 -1
  38. package/dist/pages/settings/users/notifications.js.map +1 -1
  39. package/dist/pages/settings/users/quick-actions.d.ts +1 -0
  40. package/dist/pages/settings/users/quick-actions.js +22 -0
  41. package/dist/pages/settings/users/quick-actions.js.map +1 -0
  42. package/dist/routes/auth/signup.d.ts +2 -0
  43. package/dist/routes/auth/signup.js +7 -2
  44. package/dist/routes/auth/signup.js.map +1 -1
  45. package/dist/test/integration/invite-extension-edit.test.d.ts +1 -0
  46. package/dist/test/integration/invite-extension-edit.test.js +122 -0
  47. package/dist/test/integration/invite-extension-edit.test.js.map +1 -0
  48. package/dist/test/integration/saas-mode-ownership.test.d.ts +1 -0
  49. package/dist/test/integration/saas-mode-ownership.test.js +81 -0
  50. package/dist/test/integration/saas-mode-ownership.test.js.map +1 -0
  51. package/dist/test/integration/tenant-lifecycle.test.js +38 -0
  52. package/dist/test/integration/tenant-lifecycle.test.js.map +1 -1
  53. package/dist/test/unit/implementations/dms/layout-banners.test.d.ts +1 -0
  54. package/dist/test/unit/implementations/dms/layout-banners.test.js +217 -0
  55. package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -0
  56. package/dist/test/unit/implementations/dms/quick-actions-button.test.d.ts +1 -0
  57. package/dist/test/unit/implementations/dms/quick-actions-button.test.js +220 -0
  58. package/dist/test/unit/implementations/dms/quick-actions-button.test.js.map +1 -0
  59. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +1 -1
  60. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  61. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.d.ts +1 -0
  62. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js +37 -0
  63. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js.map +1 -0
  64. package/dist/test/unit/interfaces/dms/invite-extension.test.js +138 -12
  65. package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
  66. package/dist/test/unit/interfaces/dms/password-policy.test.d.ts +1 -0
  67. package/dist/test/unit/interfaces/dms/password-policy.test.js +43 -0
  68. package/dist/test/unit/interfaces/dms/password-policy.test.js.map +1 -0
  69. package/dist/test/unit/interfaces/dms/saas-mode.test.d.ts +1 -0
  70. package/dist/test/unit/interfaces/dms/saas-mode.test.js +42 -0
  71. package/dist/test/unit/interfaces/dms/saas-mode.test.js.map +1 -0
  72. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.d.ts +1 -0
  73. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js +53 -0
  74. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js.map +1 -0
  75. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.d.ts +1 -0
  76. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js +80 -0
  77. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js.map +1 -0
  78. package/dist/test/unit/pages/settings/invite-member-quick-action.test.d.ts +1 -0
  79. package/dist/test/unit/pages/settings/invite-member-quick-action.test.js +104 -0
  80. package/dist/test/unit/pages/settings/invite-member-quick-action.test.js.map +1 -0
  81. package/dist/test/unit/pages/settings/notifications-access-gate.test.d.ts +1 -0
  82. package/dist/test/unit/pages/settings/notifications-access-gate.test.js +78 -0
  83. package/dist/test/unit/pages/settings/notifications-access-gate.test.js.map +1 -0
  84. package/dist/test/unit/signup-account-guard.test.js +20 -2
  85. package/dist/test/unit/signup-account-guard.test.js.map +1 -1
  86. package/dist/test/unit/utils/admin-invite-email.test.d.ts +1 -0
  87. package/dist/test/unit/utils/admin-invite-email.test.js +69 -0
  88. package/dist/test/unit/utils/admin-invite-email.test.js.map +1 -0
  89. package/dist/utils/admin-invite-email.d.ts +14 -0
  90. package/dist/utils/admin-invite-email.js +51 -0
  91. package/dist/utils/admin-invite-email.js.map +1 -0
  92. package/dist/utils/index.d.ts +0 -1
  93. package/dist/utils/index.js +0 -1
  94. package/dist/utils/index.js.map +1 -1
  95. package/dist/validation/auth.schema.d.ts +3 -3
  96. package/dist/validation/auth.schema.js +1 -1
  97. package/dist/validation/auth.schema.js.map +1 -1
  98. package/dist/validation/password.d.ts +0 -1
  99. package/dist/validation/password.js +6 -3
  100. package/dist/validation/password.js.map +1 -1
  101. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +37 -8
  102. package/frontend-vue/layers/dms-auth/i18n/locales/auth-en-GB.json +3 -1
  103. package/frontend-vue/layers/dms-auth/i18n/locales/auth-fr-FR.json +3 -1
  104. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +4 -0
  105. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +4 -0
  106. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +50 -0
  107. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +16 -13
  108. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +2 -2
  109. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +11 -5
  110. package/frontend-vue/layers/dms-layout/app/build/components/notification/widgetRequest.ts +20 -0
  111. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +59 -0
  112. package/frontend-vue/layers/dms-layout/app/composables/page/useQuickActions.ts +15 -2
  113. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -0
  114. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +54 -9
  115. package/frontend-vue/layers/dms-layout/app/error.vue +17 -2
  116. package/frontend-vue/layers/dms-layout/app/types/page.ts +22 -0
  117. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +48 -0
  118. package/frontend-vue/layers/dms-layout/app/utils/favorites.ts +94 -0
  119. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +21 -2
  120. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +21 -2
  121. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/index.ts +1 -1
  122. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/{useOnboardingMiddleware.ts → useOnboarding.ts} +0 -12
  123. package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +4 -1
  124. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +21 -4
  125. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +13 -3
  126. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +13 -3
  127. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +2 -7
  128. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/columnVisibility.ts +13 -0
  129. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  130. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +1 -1
  131. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +1 -1
  132. package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +2 -2
  133. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +6 -16
  134. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +2 -2
  135. package/frontend-vue/layers/dms-ui/app/components/chart/internal/TrendBadge.vue +1 -1
  136. package/frontend-vue/layers/dms-ui/app/components/copy/CopyButton.vue +1 -1
  137. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +3 -3
  138. package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +5 -1
  139. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +1 -1
  140. package/frontend-vue/layers/dms-ui/app/components/status/StatusSummary.vue +1 -1
  141. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +35 -11
  142. package/frontend-vue/layers/dms-ui/app/components/tree/Tree.vue +10 -9
  143. package/frontend-vue/layers/dms-ui/app/composables/chart/donutRecords.ts +26 -0
  144. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +4 -2
  145. package/frontend-vue/layers/dms-ui/app/composables/tab/useTab.ts +3 -3
  146. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/index.ts +17 -4
  147. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewEscape.ts +22 -0
  148. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/column.ts +1 -1
  149. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +2 -0
  150. package/frontend-vue/layers/dms-ui/app/composables/tree/shortcuts/index.ts +19 -4
  151. package/frontend-vue/layers/dms-ui/app/composables/tree/shortcuts/treeEscape.ts +21 -0
  152. package/frontend-vue/layers/dms-ui/app/types/quick-actions.ts +38 -3
  153. package/frontend-vue/layers/dms-ui/app/utils/overlayFocus.ts +16 -0
  154. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -2
  155. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -2
  156. package/frontend-vue/package.json +1 -1
  157. package/frontend-vue/pnpm-lock.yaml +5 -5
  158. package/frontend-vue/tests/admin-invite-email.test.ts +86 -0
  159. package/frontend-vue/tests/breadcrumb-target.test.ts +50 -0
  160. package/frontend-vue/tests/chart-nested-donut.test.ts +97 -0
  161. package/frontend-vue/tests/error-page-message.test.ts +83 -0
  162. package/frontend-vue/tests/favorite-page.test.ts +104 -0
  163. package/frontend-vue/tests/icon-aria-hidden.test.ts +29 -0
  164. package/frontend-vue/tests/layout-banners.test.ts +224 -0
  165. package/frontend-vue/tests/notification-popover-errors.test.ts +147 -0
  166. package/frontend-vue/tests/onboarding-middleware.test.ts +103 -0
  167. package/frontend-vue/tests/password-strength-meter.test.ts +33 -0
  168. package/frontend-vue/tests/quick-action-intent.test.ts +74 -0
  169. package/frontend-vue/tests/quick-actions.test.ts +23 -0
  170. package/frontend-vue/tests/signup-invitation-link.test.ts +126 -0
  171. package/frontend-vue/tests/skeleton-paragraph.test.ts +30 -0
  172. package/frontend-vue/tests/tab-without-component-id.test.ts +51 -0
  173. package/frontend-vue/tests/table-column-visibility.test.ts +20 -0
  174. package/frontend-vue/tests/table-view-escape-shortcut.test.ts +141 -0
  175. package/frontend-vue/tests/tree-escape-shortcut.test.ts +138 -0
  176. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  177. package/package.json +2 -2
  178. package/skills/dms/REFERENCE.md +1 -1
@@ -0,0 +1,147 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
3
+ import {
4
+ createApp,
5
+ defineComponent,
6
+ h,
7
+ nextTick,
8
+ onMounted,
9
+ onUnmounted,
10
+ ref,
11
+ watch,
12
+ type App,
13
+ } from "vue";
14
+ import NotificationPopover from "../layers/dms-layout/app/build/components/notification/NotificationPopover.vue";
15
+
16
+ const HTTP_FORBIDDEN = 403;
17
+ const UNREAD_COUNT = 4;
18
+
19
+ interface NotificationsStub {
20
+ unreadCount: ReturnType<typeof ref<number>>;
21
+ fetchUnreadCount: ReturnType<typeof vi.fn>;
22
+ fetchNotifications: ReturnType<typeof vi.fn>;
23
+ }
24
+
25
+ let app: App;
26
+ let host: HTMLDivElement;
27
+ let errorHandler: ReturnType<typeof vi.fn>;
28
+ let notifications: NotificationsStub;
29
+
30
+ function forbidden(): Promise<never> {
31
+ return Promise.reject(
32
+ Object.assign(new Error("tenant.suspended"), {
33
+ response: { status: HTTP_FORBIDDEN },
34
+ }),
35
+ );
36
+ }
37
+
38
+ const Popover = defineComponent({
39
+ props: { open: { type: Boolean, default: false } },
40
+ emits: ["update:open"],
41
+ setup:
42
+ (props, { emit, slots }) =>
43
+ () =>
44
+ h("div", [
45
+ h(
46
+ "div",
47
+ { "data-trigger": "", onClick: () => emit("update:open", true) },
48
+ slots.default?.(),
49
+ ),
50
+ props.open ? slots.content?.() : null,
51
+ ]),
52
+ });
53
+
54
+ const Passthrough = defineComponent({
55
+ setup:
56
+ (_, { slots }) =>
57
+ () =>
58
+ h("div", slots.default?.()),
59
+ });
60
+
61
+ function installRuntime(): void {
62
+ Object.entries({ ref, watch, nextTick, onMounted, onUnmounted }).forEach(
63
+ ([key, value]) => vi.stubGlobal(key, value),
64
+ );
65
+ vi.stubGlobal("useI18n", () => ({ t: (key: string) => key, locale: "en" }));
66
+ vi.stubGlobal("useUserSession", () => ({ loggedIn: ref(true) }));
67
+ vi.stubGlobal("useTranslation", () => ({
68
+ processI18n: (value: string) => value,
69
+ }));
70
+ vi.stubGlobal("useNotifications", () => ({
71
+ ...notifications,
72
+ notifications: ref([]),
73
+ hasMore: ref(false),
74
+ markAllAsRead: vi.fn(),
75
+ }));
76
+ vi.stubGlobal("useInfiniteScroll", () => ({
77
+ isLoadingMore: ref(false),
78
+ setupObserver: vi.fn(),
79
+ disconnectObserver: vi.fn(),
80
+ }));
81
+ vi.stubGlobal("NotificationEvents", { NOTIFICATION_RECEIVED: "received" });
82
+ vi.stubGlobal("FormEvents", { SUBMIT_SUCCESS: "submit-success" });
83
+ vi.stubGlobal("navigateDms", vi.fn());
84
+ }
85
+
86
+ async function mountPopover(): Promise<void> {
87
+ app = createApp(NotificationPopover);
88
+ app.config.errorHandler = errorHandler;
89
+ app.config.globalProperties.$t = (key: string) => key;
90
+ app.component("UPopover", Popover);
91
+ app.component("UButton", Passthrough);
92
+ app.component("USeparator", Passthrough);
93
+ app.component("UIcon", Passthrough);
94
+ app.mount(host);
95
+ await vi.waitFor(() =>
96
+ expect(notifications.fetchUnreadCount).toHaveBeenCalled(),
97
+ );
98
+ await nextTick();
99
+ }
100
+
101
+ beforeEach(() => {
102
+ errorHandler = vi.fn();
103
+ notifications = {
104
+ unreadCount: ref(0),
105
+ fetchUnreadCount: vi.fn(),
106
+ fetchNotifications: vi.fn(),
107
+ };
108
+ vi.spyOn(console, "warn").mockImplementation(() => {});
109
+ installRuntime();
110
+ host = document.createElement("div");
111
+ });
112
+
113
+ afterEach(() => {
114
+ app?.unmount();
115
+ vi.unstubAllGlobals();
116
+ vi.restoreAllMocks();
117
+ });
118
+
119
+ it("shows the unread badge when the count loads", async () => {
120
+ notifications.fetchUnreadCount.mockImplementation(async () => {
121
+ notifications.unreadCount.value = UNREAD_COUNT;
122
+ });
123
+ await mountPopover();
124
+ expect(host.textContent).toContain(String(UNREAD_COUNT));
125
+ });
126
+
127
+ it("hides the badge instead of failing the page when the count is refused", async () => {
128
+ notifications.unreadCount.value = UNREAD_COUNT;
129
+ notifications.fetchUnreadCount.mockImplementation(forbidden);
130
+ await mountPopover();
131
+ await vi.waitFor(() => expect(console.warn).toHaveBeenCalled());
132
+ expect(errorHandler).not.toHaveBeenCalled();
133
+ expect(notifications.unreadCount.value).toBe(0);
134
+ expect(host.textContent).not.toContain(String(UNREAD_COUNT));
135
+ });
136
+
137
+ it("keeps the page alive when the notification list is refused on open", async () => {
138
+ notifications.fetchNotifications.mockImplementation(forbidden);
139
+ await mountPopover();
140
+ host.querySelector<HTMLElement>("[data-trigger]")!.click();
141
+ await vi.waitFor(() =>
142
+ expect(notifications.fetchNotifications).toHaveBeenCalled(),
143
+ );
144
+ await vi.waitFor(() => expect(console.warn).toHaveBeenCalled());
145
+ expect(errorHandler).not.toHaveBeenCalled();
146
+ expect(host.textContent).toContain("notification.dropdown.no_notifications");
147
+ });
@@ -0,0 +1,103 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ type Middleware = (to: { path: string }) => Promise<unknown>;
5
+
6
+ const navigateDms = vi.fn();
7
+ const onboarding = vi.fn();
8
+ const session = { loggedIn: { value: false } };
9
+
10
+ beforeEach(() => {
11
+ vi.resetModules();
12
+ vi.stubGlobal("defineDmsMiddleware", (handler: Middleware) => handler);
13
+ vi.stubGlobal("navigateDms", navigateDms);
14
+ vi.stubGlobal("useOnboarding", onboarding);
15
+ vi.stubGlobal("useUserSession", () => session);
16
+ vi.stubGlobal("useHomepage", () => "/dashboard");
17
+ });
18
+
19
+ afterEach(() => {
20
+ session.loggedIn.value = false;
21
+ navigateDms.mockReset();
22
+ onboarding.mockReset();
23
+ vi.unstubAllGlobals();
24
+ });
25
+
26
+ async function loadMiddleware(): Promise<Middleware> {
27
+ const module = await import(
28
+ "../layers/dms-onboarding/app/middleware/onboarding.global"
29
+ );
30
+ return module.default as unknown as Middleware;
31
+ }
32
+
33
+ describe("onboarding middleware", () => {
34
+ it("sends every route to the wizard while no admin exists", async () => {
35
+ onboarding.mockResolvedValue({ hasOnboarded: false });
36
+ const middleware = await loadMiddleware();
37
+
38
+ await middleware({ path: "/settings" });
39
+
40
+ expect(navigateDms).toHaveBeenCalledWith("/onboarding");
41
+ });
42
+
43
+ it("lets the wizard render while no admin exists", async () => {
44
+ onboarding.mockResolvedValue({ hasOnboarded: false });
45
+ const middleware = await loadMiddleware();
46
+
47
+ await middleware({ path: "/onboarding" });
48
+
49
+ expect(navigateDms).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it("sends a signed-in user away from the finished wizard to the homepage", async () => {
53
+ onboarding.mockResolvedValue({ hasOnboarded: true });
54
+ session.loggedIn.value = true;
55
+ const middleware = await loadMiddleware();
56
+
57
+ await middleware({ path: "/onboarding" });
58
+
59
+ expect(navigateDms).toHaveBeenCalledWith("/dashboard", { replace: true });
60
+ });
61
+
62
+ it("sends an anonymous visitor away from the finished wizard to the login page", async () => {
63
+ onboarding.mockResolvedValue({ hasOnboarded: true });
64
+ const middleware = await loadMiddleware();
65
+
66
+ await middleware({ path: "/onboarding" });
67
+
68
+ expect(navigateDms).toHaveBeenCalledWith("/auth", { replace: true });
69
+ });
70
+
71
+ it("leaves other routes alone once onboarding is done", async () => {
72
+ onboarding.mockResolvedValue({ hasOnboarded: true });
73
+ const middleware = await loadMiddleware();
74
+
75
+ await middleware({ path: "/settings" });
76
+
77
+ expect(navigateDms).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it("does not block navigation when the onboarding status is unavailable", async () => {
81
+ onboarding.mockRejectedValue(new Error("offline"));
82
+ const middleware = await loadMiddleware();
83
+
84
+ await middleware({ path: "/onboarding" });
85
+
86
+ expect(navigateDms).not.toHaveBeenCalled();
87
+ });
88
+ });
89
+
90
+ describe("onboarding page", () => {
91
+ // Global middleware runs on a direct load only for a page declaring its
92
+ // meta: without it, a finished wizard would render instead of redirecting.
93
+ it("declares its meta so the onboarding middleware runs on a direct load", () => {
94
+ const page = readFileSync(
95
+ new URL(
96
+ "../layers/dms-onboarding/app/custom-pages/onboarding.vue",
97
+ import.meta.url,
98
+ ),
99
+ "utf8",
100
+ );
101
+ expect(page).toMatch(/defineDmsPageMeta\(/);
102
+ });
103
+ });
@@ -0,0 +1,33 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, expect, it } from "vitest";
3
+ import { createApp, defineComponent, h, type App } from "vue";
4
+ import PasswordStrength from "../layers/dms-ui/app/components/PasswordStrength.vue";
5
+
6
+ let app: App | undefined;
7
+
8
+ afterEach(() => app?.unmount());
9
+
10
+ it("scales the meter to the number of requirements", () => {
11
+ const host = document.createElement("div");
12
+ const strength = Array.from({ length: 5 }, (_, index) => ({
13
+ met: true,
14
+ text: `requirement ${index}`,
15
+ }));
16
+
17
+ app = createApp(PasswordStrength, { color: "success", score: 5, strength });
18
+ app.config.globalProperties.$t = (key: string) => key;
19
+ app.component(
20
+ "UProgress",
21
+ defineComponent({
22
+ props: { modelValue: Number, max: Number },
23
+ setup: (props) => () =>
24
+ h("progress", { value: props.modelValue, max: props.max }),
25
+ }),
26
+ );
27
+ app.component("UIcon", defineComponent({ render: () => null }));
28
+ app.mount(host);
29
+
30
+ const meter = host.querySelector("progress")!;
31
+ expect(meter.getAttribute("max")).toBe("5");
32
+ expect(meter.getAttribute("value")).toBe("5");
33
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ QUICK_ACTION_ADD,
4
+ QUICK_ACTION_BUTTON,
5
+ QUICK_ACTION_BUTTON_KEY,
6
+ QUICK_ACTION_COMPONENT_KEY,
7
+ QUICK_ACTION_QUERY_KEY,
8
+ readQuickActionIntent,
9
+ } from "../layers/dms-ui/app/types/quick-actions";
10
+
11
+ const COMPONENT_ID = "table";
12
+
13
+ describe("readQuickActionIntent", () => {
14
+ it("reads an open-form intent addressed to the table view", () => {
15
+ const intent = readQuickActionIntent(
16
+ {
17
+ [QUICK_ACTION_QUERY_KEY]: QUICK_ACTION_ADD,
18
+ [QUICK_ACTION_COMPONENT_KEY]: COMPONENT_ID,
19
+ },
20
+ COMPONENT_ID,
21
+ );
22
+
23
+ expect(intent).toEqual({ kind: "add" });
24
+ });
25
+
26
+ it("reads which button a button intent presses", () => {
27
+ const intent = readQuickActionIntent(
28
+ {
29
+ [QUICK_ACTION_QUERY_KEY]: QUICK_ACTION_BUTTON,
30
+ [QUICK_ACTION_COMPONENT_KEY]: COMPONENT_ID,
31
+ [QUICK_ACTION_BUTTON_KEY]: "invite",
32
+ },
33
+ COMPONENT_ID,
34
+ );
35
+
36
+ expect(intent).toEqual({ kind: "button", button: "invite" });
37
+ });
38
+
39
+ it("ignores a button intent that names no button", () => {
40
+ const intent = readQuickActionIntent(
41
+ {
42
+ [QUICK_ACTION_QUERY_KEY]: QUICK_ACTION_BUTTON,
43
+ [QUICK_ACTION_COMPONENT_KEY]: COMPONENT_ID,
44
+ },
45
+ COMPONENT_ID,
46
+ );
47
+
48
+ expect(intent).toBeUndefined();
49
+ });
50
+
51
+ it("ignores an intent addressed to another table view", () => {
52
+ const intent = readQuickActionIntent(
53
+ {
54
+ [QUICK_ACTION_QUERY_KEY]: QUICK_ACTION_ADD,
55
+ [QUICK_ACTION_COMPONENT_KEY]: "other-table",
56
+ },
57
+ COMPONENT_ID,
58
+ );
59
+
60
+ expect(intent).toBeUndefined();
61
+ });
62
+
63
+ it("ignores an unknown intent", () => {
64
+ const intent = readQuickActionIntent(
65
+ {
66
+ [QUICK_ACTION_QUERY_KEY]: "unknown",
67
+ [QUICK_ACTION_COMPONENT_KEY]: COMPONENT_ID,
68
+ },
69
+ COMPONENT_ID,
70
+ );
71
+
72
+ expect(intent).toBeUndefined();
73
+ });
74
+ });
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { dispatchQuickActionTarget } from "../layers/dms-layout/app/composables/page/useQuickActions";
3
3
  import {
4
4
  QUICK_ACTION_ADD,
5
+ QUICK_ACTION_BUTTON,
6
+ QUICK_ACTION_BUTTON_KEY,
5
7
  QUICK_ACTION_COMPONENT_KEY,
6
8
  QUICK_ACTION_QUERY_KEY,
7
9
  } from "../layers/dms-ui/app/types/quick-actions";
@@ -29,4 +31,25 @@ describe("dispatchQuickActionTarget", () => {
29
31
  },
30
32
  });
31
33
  });
34
+
35
+ it("routes button actions to the target table with the button to press", () => {
36
+ const navigateDms = vi.fn();
37
+ vi.stubGlobal("navigateDms", navigateDms);
38
+
39
+ dispatchQuickActionTarget({
40
+ type: "button",
41
+ to: "/settings/user/members",
42
+ component: "table",
43
+ button: "invite",
44
+ });
45
+
46
+ expect(navigateDms).toHaveBeenCalledWith({
47
+ path: "/settings/user/members",
48
+ query: {
49
+ [QUICK_ACTION_QUERY_KEY]: QUICK_ACTION_BUTTON,
50
+ [QUICK_ACTION_COMPONENT_KEY]: "table",
51
+ [QUICK_ACTION_BUTTON_KEY]: "invite",
52
+ },
53
+ });
54
+ });
32
55
  });
@@ -0,0 +1,126 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
3
+ import {
4
+ computed,
5
+ createApp,
6
+ defineComponent,
7
+ h,
8
+ reactive,
9
+ ref,
10
+ type App,
11
+ } from "vue";
12
+ import * as z from "zod";
13
+ import SignupPage from "../layers/dms-auth/app/custom-pages/auth/signup.vue";
14
+
15
+ const route = reactive<{ query: Record<string, unknown> }>({ query: {} });
16
+ let app: App;
17
+ let host: HTMLDivElement;
18
+ const locale = ref("en");
19
+ const setLocale = vi.fn(async (code: string) => {
20
+ locale.value = code;
21
+ });
22
+
23
+ const Passthrough = defineComponent({
24
+ inheritAttrs: false,
25
+ setup:
26
+ (_, { attrs, slots }) =>
27
+ () =>
28
+ h("div", { "data-testid": attrs["data-testid"] }, slots.default?.()),
29
+ });
30
+
31
+ const LinkButton = defineComponent({
32
+ props: { label: String, to: String },
33
+ setup: (props) => () => h("a", { href: props.to }, props.label),
34
+ });
35
+
36
+ function installRuntime() {
37
+ Object.entries({ computed, ref, reactive }).forEach(([key, value]) =>
38
+ vi.stubGlobal(key, value),
39
+ );
40
+ vi.stubGlobal("useI18n", () => ({
41
+ locale,
42
+ locales: ref([{ code: "en" }, { code: "fr" }]),
43
+ setLocale,
44
+ }));
45
+ vi.stubGlobal("useDmsRuntimeConfig", () => ({
46
+ public: { dms: { mustValidateEmail: false } },
47
+ }));
48
+ vi.stubGlobal("useHomepage", () => "/");
49
+ vi.stubGlobal("useDmsRoute", () => route);
50
+ vi.stubGlobal("passwordSchema", z.string());
51
+ vi.stubGlobal("usePasswordStrength", () => ({
52
+ strength: ref([]),
53
+ score: ref(0),
54
+ color: ref("error"),
55
+ }));
56
+ }
57
+
58
+ function mountSignup(query: Record<string, unknown>) {
59
+ route.query = query;
60
+ app = createApp(SignupPage);
61
+ app.config.globalProperties.$t = (key: string) => key;
62
+ for (const name of [
63
+ "DmsCard",
64
+ "UIcon",
65
+ "UForm",
66
+ "UFormField",
67
+ "UInput",
68
+ "DmsOAuthButtons",
69
+ "DmsPasswordStrength",
70
+ "DmsLink",
71
+ ]) {
72
+ app.component(name, Passthrough);
73
+ }
74
+ app.component("UButton", LinkButton);
75
+ app.mount(host);
76
+ }
77
+
78
+ beforeEach(() => {
79
+ locale.value = "en";
80
+ setLocale.mockClear();
81
+ installRuntime();
82
+ host = document.createElement("div");
83
+ });
84
+ afterEach(() => {
85
+ app?.unmount();
86
+ vi.unstubAllGlobals();
87
+ });
88
+
89
+ it.each([{}, { token: "" }, { token: ["a", "b"] }])(
90
+ "explains an invitation link without its token (%o)",
91
+ (query) => {
92
+ expect(() => mountSignup(query)).not.toThrow();
93
+ expect(
94
+ host.querySelector('[data-testid="signup-invalid-invitation"]'),
95
+ ).not.toBe(null);
96
+ expect(host.textContent).toContain(
97
+ "page.signup.invalid_invitation_description",
98
+ );
99
+ expect(host.querySelector('a[href="/auth"]')?.textContent).toBe(
100
+ "button.login",
101
+ );
102
+ },
103
+ );
104
+
105
+ it("shows the signup form for an invitation link with its token", () => {
106
+ mountSignup({ token: "invite-token", email: "new@local.test" });
107
+ expect(host.querySelector('[data-testid="signup-invalid-invitation"]')).toBe(
108
+ null,
109
+ );
110
+ expect(host.textContent).toContain("page.signup.create_account_title");
111
+ });
112
+
113
+ it("opens in the language the invitation was written in", () => {
114
+ mountSignup({ token: "invite-token", email: "new@local.test", lang: "fr" });
115
+ expect(setLocale).toHaveBeenCalledWith("fr");
116
+ expect(locale.value).toBe("fr");
117
+ });
118
+
119
+ it.each([{}, { lang: "en" }, { lang: "de" }, { lang: ["fr", "en"] }])(
120
+ "keeps the current language for an invitation link carrying %o",
121
+ (query) => {
122
+ mountSignup({ token: "invite-token", email: "new@local.test", ...query });
123
+ expect(setLocale).not.toHaveBeenCalled();
124
+ expect(locale.value).toBe("en");
125
+ },
126
+ );
@@ -0,0 +1,30 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const PARAGRAPH = /<p\b[^>]*>(.*?)<\/p>/gs;
6
+ const SKELETON = /<USkeleton\b/;
7
+
8
+ function walk(directory: string): string[] {
9
+ return readdirSync(directory).flatMap((name) => {
10
+ const path = join(directory, name);
11
+ return statSync(path).isDirectory() ? walk(path) : [path];
12
+ });
13
+ }
14
+
15
+ describe("skeleton placement", () => {
16
+ // The skeleton renders a <div>. The browser's parser closes an open <p>
17
+ // before a <div>, so the server markup no longer matches the client tree
18
+ // and hydration reports a mismatch.
19
+ it("keeps skeletons out of paragraphs", () => {
20
+ const offenders = walk(join(process.cwd(), "layers"))
21
+ .filter((path) => path.endsWith(".vue"))
22
+ .flatMap((path) =>
23
+ [...readFileSync(path, "utf8").matchAll(PARAGRAPH)]
24
+ .filter(([, content]) => SKELETON.test(content ?? ""))
25
+ .map(() => relative(process.cwd(), path)),
26
+ );
27
+
28
+ expect(offenders).toEqual([]);
29
+ });
30
+ });
@@ -0,0 +1,51 @@
1
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
2
+ import { computed, nextTick, reactive, ref, watch } from "vue";
3
+ import { useTab } from "../layers/dms-ui/app/composables/tab/useTab";
4
+ import { TabEvents } from "../layers/dms-ui/app/composables/tab/types/events";
5
+ import type { TabProps } from "../layers/dms-ui/app/composables/tab/types";
6
+
7
+ const TAB_COUNT = 3;
8
+ const sendComponentEvent = vi.fn();
9
+
10
+ beforeEach(() => {
11
+ sendComponentEvent.mockClear();
12
+ Object.entries({ computed, ref, watch, TabEvents }).forEach(([key, value]) =>
13
+ vi.stubGlobal(key, value),
14
+ );
15
+ vi.stubGlobal("isNumber", (value: unknown) => typeof value === "number");
16
+ vi.stubGlobal("isString", (value: unknown) => typeof value === "string");
17
+ vi.stubGlobal("useComponentEvent", () => ({ sendComponentEvent }));
18
+ vi.stubGlobal("useWatch", () => ({ isLoading: ref(false), state: ref({}) }));
19
+ vi.stubGlobal("useDmsRoute", () => reactive({ query: {} }));
20
+ vi.stubGlobal("useDmsRouter", () => ({ push: vi.fn() }));
21
+ });
22
+ afterEach(() => {
23
+ vi.unstubAllGlobals();
24
+ });
25
+
26
+ it("switches tabs without announcing them when no component owns the set", async () => {
27
+ const { activeTab, goToTab } = useTab(TAB_COUNT, { items: [] } as TabProps);
28
+
29
+ goToTab(1);
30
+ await nextTick();
31
+
32
+ expect(activeTab.value).toBe("1");
33
+ expect(sendComponentEvent).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it("announces a tab change for a registered component", async () => {
37
+ const { goToTab } = useTab(TAB_COUNT, {
38
+ items: [],
39
+ componentId: "settings.tabs",
40
+ pageId: "settings",
41
+ } as TabProps);
42
+
43
+ goToTab(2);
44
+ await nextTick();
45
+
46
+ expect(sendComponentEvent).toHaveBeenCalledWith(
47
+ TabEvents.TAB_CHANGE,
48
+ "settings.tabs",
49
+ { previousTab: 0, currentTab: 2 },
50
+ );
51
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildInitialColumnVisibility } from "../layers/dms-ui/app/build/composables/table-view/utils/columnVisibility";
3
+
4
+ describe("buildInitialColumnVisibility", () => {
5
+ it("hides the columns declared isVisible: false", () => {
6
+ expect(
7
+ buildInitialColumnVisibility([
8
+ { id: "number" },
9
+ { id: "amount", isVisible: true },
10
+ { id: "stripeId", isVisible: false },
11
+ ]),
12
+ ).toEqual({ number: true, amount: true, stripeId: false });
13
+ });
14
+
15
+ it("keeps every column visible when none opts out", () => {
16
+ expect(buildInitialColumnVisibility([{ id: "name" }])).toEqual({
17
+ name: true,
18
+ });
19
+ });
20
+ });