@antelopejs/dms 0.4.5 → 0.4.6
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.
- package/CHANGELOG.md +27 -0
- package/dist/hooks/invite-extensions.js +2 -2
- package/dist/hooks/invite-extensions.js.map +1 -1
- package/dist/implementations/dms/layout-banners.d.ts +18 -0
- package/dist/implementations/dms/layout-banners.js +60 -0
- package/dist/implementations/dms/layout-banners.js.map +1 -0
- package/dist/implementations/dms/page.d.ts +2 -0
- package/dist/implementations/dms/page.js +82 -27
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms/quick-actions.d.ts +5 -0
- package/dist/implementations/dms/quick-actions.js.map +1 -1
- package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
- package/dist/implementations/dms/resolver-timeout.js +13 -0
- package/dist/implementations/dms/resolver-timeout.js.map +1 -0
- package/dist/implementations/dms/saas-mode.d.ts +8 -0
- package/dist/implementations/dms/saas-mode.js +20 -0
- package/dist/implementations/dms/saas-mode.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +2 -1
- package/dist/implementations/dms-auth/index.js +19 -8
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/implementations/dms-html-render/service-jwt.js +2 -1
- package/dist/implementations/dms-html-render/service-jwt.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/settings/users/index.d.ts +1 -0
- package/dist/pages/settings/users/index.js +1 -0
- package/dist/pages/settings/users/index.js.map +1 -1
- package/dist/pages/settings/users/invite-extension-routes.d.ts +4 -0
- package/dist/pages/settings/users/invite-extension-routes.js +57 -0
- package/dist/pages/settings/users/invite-extension-routes.js.map +1 -0
- package/dist/pages/settings/users/invites.d.ts +2 -1
- package/dist/pages/settings/users/invites.js +17 -5
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.d.ts +4 -1
- package/dist/pages/settings/users/members.js +10 -6
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -1
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/pages/settings/users/quick-actions.d.ts +1 -0
- package/dist/pages/settings/users/quick-actions.js +22 -0
- package/dist/pages/settings/users/quick-actions.js.map +1 -0
- package/dist/routes/auth/signup.d.ts +2 -0
- package/dist/routes/auth/signup.js +7 -2
- package/dist/routes/auth/signup.js.map +1 -1
- package/dist/test/integration/invite-extension-edit.test.d.ts +1 -0
- package/dist/test/integration/invite-extension-edit.test.js +122 -0
- package/dist/test/integration/invite-extension-edit.test.js.map +1 -0
- package/dist/test/integration/saas-mode-ownership.test.d.ts +1 -0
- package/dist/test/integration/saas-mode-ownership.test.js +81 -0
- package/dist/test/integration/saas-mode-ownership.test.js.map +1 -0
- package/dist/test/integration/tenant-lifecycle.test.js +38 -0
- package/dist/test/integration/tenant-lifecycle.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/layout-banners.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/layout-banners.test.js +217 -0
- package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -0
- package/dist/test/unit/implementations/dms/quick-actions-button.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/quick-actions-button.test.js +220 -0
- package/dist/test/unit/implementations/dms/quick-actions-button.test.js.map +1 -0
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +1 -1
- package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/service-jwt.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js +37 -0
- package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/invite-extension.test.js +138 -12
- package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/saas-mode.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/saas-mode.test.js +42 -0
- package/dist/test/unit/interfaces/dms/saas-mode.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/tenant-invite-email.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js +53 -0
- package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js +80 -0
- package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js.map +1 -0
- package/dist/test/unit/pages/settings/invite-member-quick-action.test.d.ts +1 -0
- package/dist/test/unit/pages/settings/invite-member-quick-action.test.js +104 -0
- package/dist/test/unit/pages/settings/invite-member-quick-action.test.js.map +1 -0
- package/dist/test/unit/pages/settings/notifications-access-gate.test.d.ts +1 -0
- package/dist/test/unit/pages/settings/notifications-access-gate.test.js +78 -0
- package/dist/test/unit/pages/settings/notifications-access-gate.test.js.map +1 -0
- package/dist/test/unit/signup-account-guard.test.js +20 -2
- package/dist/test/unit/signup-account-guard.test.js.map +1 -1
- package/dist/test/unit/utils/admin-invite-email.test.d.ts +1 -0
- package/dist/test/unit/utils/admin-invite-email.test.js +69 -0
- package/dist/test/unit/utils/admin-invite-email.test.js.map +1 -0
- package/dist/utils/admin-invite-email.d.ts +14 -0
- package/dist/utils/admin-invite-email.js +51 -0
- package/dist/utils/admin-invite-email.js.map +1 -0
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/validation/auth.schema.d.ts +3 -3
- package/dist/validation/auth.schema.js +1 -1
- package/dist/validation/auth.schema.js.map +1 -1
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +37 -8
- package/frontend-vue/layers/dms-auth/i18n/locales/auth-en-GB.json +3 -1
- package/frontend-vue/layers/dms-auth/i18n/locales/auth-fr-FR.json +3 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +50 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +16 -13
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +2 -2
- package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +11 -5
- package/frontend-vue/layers/dms-layout/app/build/components/notification/widgetRequest.ts +20 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +59 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useQuickActions.ts +15 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +54 -9
- package/frontend-vue/layers/dms-layout/app/error.vue +17 -2
- package/frontend-vue/layers/dms-layout/app/types/page.ts +22 -0
- package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +48 -0
- package/frontend-vue/layers/dms-layout/app/utils/favorites.ts +94 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +21 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +21 -2
- package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/index.ts +1 -1
- package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/{useOnboardingMiddleware.ts → useOnboarding.ts} +0 -12
- package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +4 -1
- package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +21 -4
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +2 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/columnVisibility.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +6 -16
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/chart/internal/TrendBadge.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/copy/CopyButton.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +3 -3
- package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/status/StatusSummary.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +33 -10
- package/frontend-vue/layers/dms-ui/app/composables/chart/donutRecords.ts +26 -0
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +4 -2
- package/frontend-vue/layers/dms-ui/app/composables/tab/useTab.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/column.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/types/quick-actions.ts +38 -3
- package/frontend-vue/tests/admin-invite-email.test.ts +86 -0
- package/frontend-vue/tests/breadcrumb-target.test.ts +50 -0
- package/frontend-vue/tests/chart-nested-donut.test.ts +97 -0
- package/frontend-vue/tests/error-page-message.test.ts +83 -0
- package/frontend-vue/tests/favorite-page.test.ts +104 -0
- package/frontend-vue/tests/icon-aria-hidden.test.ts +29 -0
- package/frontend-vue/tests/layout-banners.test.ts +224 -0
- package/frontend-vue/tests/notification-popover-errors.test.ts +147 -0
- package/frontend-vue/tests/onboarding-middleware.test.ts +103 -0
- package/frontend-vue/tests/password-strength-meter.test.ts +33 -0
- package/frontend-vue/tests/quick-action-intent.test.ts +74 -0
- package/frontend-vue/tests/quick-actions.test.ts +23 -0
- package/frontend-vue/tests/signup-invitation-link.test.ts +126 -0
- package/frontend-vue/tests/skeleton-paragraph.test.ts +30 -0
- package/frontend-vue/tests/tab-without-component-id.test.ts +51 -0
- package/frontend-vue/tests/table-column-visibility.test.ts +20 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/REFERENCE.md +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createApp,
|
|
5
|
+
createSSRApp,
|
|
6
|
+
defineComponent,
|
|
7
|
+
h,
|
|
8
|
+
nextTick,
|
|
9
|
+
ref,
|
|
10
|
+
type App,
|
|
11
|
+
type Ref,
|
|
12
|
+
} from "vue";
|
|
13
|
+
import { renderToString } from "vue/server-renderer";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import DashboardBanners from "../layers/dms-layout/app/build/components/layout/DashboardBanners.vue";
|
|
16
|
+
import {
|
|
17
|
+
resolveLayoutBannerPresentation,
|
|
18
|
+
useLayoutBanners,
|
|
19
|
+
} from "../layers/dms-layout/app/composables/layout/useLayoutBanners";
|
|
20
|
+
import type {
|
|
21
|
+
LayoutBanner,
|
|
22
|
+
LayoutBannerVariant,
|
|
23
|
+
SiteLayout,
|
|
24
|
+
} from "../layers/dms-layout/app/types/page";
|
|
25
|
+
|
|
26
|
+
const DISMISSED_BANNERS_COOKIE = "dms-dismissed-banners";
|
|
27
|
+
|
|
28
|
+
const siteLayout = ref<SiteLayout | undefined>();
|
|
29
|
+
let dismissedCookie: Ref<string[]>;
|
|
30
|
+
const cookieNames: string[] = [];
|
|
31
|
+
|
|
32
|
+
const banner = (extra: Partial<LayoutBanner> = {}): LayoutBanner => ({
|
|
33
|
+
key: "banner",
|
|
34
|
+
variant: "info",
|
|
35
|
+
order: 0,
|
|
36
|
+
dismissible: false,
|
|
37
|
+
text: "Heads up",
|
|
38
|
+
...extra,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function serveBanners(banners: LayoutBanner[] | undefined): void {
|
|
42
|
+
siteLayout.value = { pages: {}, categories: {}, banners };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Renders what the real alert exposes to the banner: its description slot and
|
|
46
|
+
// a close button that reports the alert closed.
|
|
47
|
+
const AlertStub = defineComponent({
|
|
48
|
+
props: {
|
|
49
|
+
color: { type: String, default: undefined },
|
|
50
|
+
icon: { type: String, default: undefined },
|
|
51
|
+
close: { type: Boolean, default: false },
|
|
52
|
+
},
|
|
53
|
+
emits: ["update:open"],
|
|
54
|
+
setup:
|
|
55
|
+
(props, { slots, emit }) =>
|
|
56
|
+
() =>
|
|
57
|
+
h("div", { "data-color": props.color, "data-icon": props.icon }, [
|
|
58
|
+
slots.description?.(),
|
|
59
|
+
props.close
|
|
60
|
+
? h("button", { onClick: () => emit("update:open", false) }, "close")
|
|
61
|
+
: null,
|
|
62
|
+
]),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const BannerContent = defineComponent({
|
|
66
|
+
props: { days: { type: Number, required: true } },
|
|
67
|
+
setup: (props) => () => h("strong", `${props.days} days left`),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function mountable(root: App): App {
|
|
71
|
+
root.component("UAlert", AlertStub);
|
|
72
|
+
root.component("BannerContent", BannerContent);
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
siteLayout.value = undefined;
|
|
78
|
+
cookieNames.length = 0;
|
|
79
|
+
dismissedCookie = ref<string[]>([]);
|
|
80
|
+
vi.stubGlobal("computed", computed);
|
|
81
|
+
vi.stubGlobal("useSiteLayout", () => ({ siteLayout }));
|
|
82
|
+
vi.stubGlobal("useDmsCookie", (name: string) => {
|
|
83
|
+
cookieNames.push(name);
|
|
84
|
+
return dismissedCookie;
|
|
85
|
+
});
|
|
86
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
87
|
+
processI18n: (key: string) => key.replace(/^\$/, "t:"),
|
|
88
|
+
}));
|
|
89
|
+
vi.stubGlobal("useLayoutBanners", useLayoutBanners);
|
|
90
|
+
vi.stubGlobal(
|
|
91
|
+
"resolveLayoutBannerPresentation",
|
|
92
|
+
resolveLayoutBannerPresentation,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
vi.unstubAllGlobals();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("resolveLayoutBannerPresentation", () => {
|
|
101
|
+
it.each([
|
|
102
|
+
["info", "status"],
|
|
103
|
+
["warning", "status"],
|
|
104
|
+
["error", "alert"],
|
|
105
|
+
] as const)("announces a %s banner as %s", (variant, role) => {
|
|
106
|
+
const presentation = resolveLayoutBannerPresentation(banner({ variant }));
|
|
107
|
+
expect(presentation.role).toBe(role);
|
|
108
|
+
expect(presentation.color).toBe(variant);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("renders an unknown variant as info rather than unstyled", () => {
|
|
112
|
+
const presentation = resolveLayoutBannerPresentation(
|
|
113
|
+
banner({ variant: "success" as LayoutBannerVariant }),
|
|
114
|
+
);
|
|
115
|
+
expect(presentation).toEqual(
|
|
116
|
+
resolveLayoutBannerPresentation(banner({ variant: "info" })),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("keeps the banner's own icon over the variant's", () => {
|
|
121
|
+
expect(
|
|
122
|
+
resolveLayoutBannerPresentation(banner({ icon: "i-ph-clock" })).icon,
|
|
123
|
+
).toBe("i-ph-clock");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("useLayoutBanners", () => {
|
|
128
|
+
it("serves nothing for a backend that predates banners", () => {
|
|
129
|
+
serveBanners(undefined);
|
|
130
|
+
expect(useLayoutBanners().banners.value).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("drops a dismissed banner, and remembers the dismissal in the cookie", () => {
|
|
134
|
+
serveBanners([
|
|
135
|
+
banner({ key: "a", dismissible: true }),
|
|
136
|
+
banner({ key: "b", dismissible: true }),
|
|
137
|
+
]);
|
|
138
|
+
const { banners, dismiss } = useLayoutBanners();
|
|
139
|
+
|
|
140
|
+
dismiss("a");
|
|
141
|
+
dismiss("a");
|
|
142
|
+
|
|
143
|
+
expect(cookieNames).toEqual([DISMISSED_BANNERS_COOKIE]);
|
|
144
|
+
expect(dismissedCookie.value).toEqual(["a"]);
|
|
145
|
+
expect(banners.value.map((entry) => entry.key)).toEqual(["b"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// A banner that turned non-dismissible (an overdue invoice) must show again
|
|
149
|
+
// even to a user who closed its dismissible predecessor under the same key.
|
|
150
|
+
it("ignores a dismissal for a banner that is not dismissible", () => {
|
|
151
|
+
dismissedCookie.value = ["a"];
|
|
152
|
+
serveBanners([banner({ key: "a", dismissible: false })]);
|
|
153
|
+
expect(useLayoutBanners().banners.value).toHaveLength(1);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("DashboardBanners", () => {
|
|
158
|
+
it("server-renders the banners in order, each with its variant's role", async () => {
|
|
159
|
+
serveBanners([
|
|
160
|
+
banner({ key: "billing", variant: "error", text: "$billing.overdue" }),
|
|
161
|
+
banner({
|
|
162
|
+
key: "trial",
|
|
163
|
+
variant: "warning",
|
|
164
|
+
component: "BannerContent",
|
|
165
|
+
props: { days: 3 },
|
|
166
|
+
text: undefined,
|
|
167
|
+
}),
|
|
168
|
+
banner({ key: "news", variant: "info", text: "New release" }),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const html = await renderToString(
|
|
172
|
+
mountable(createSSRApp(DashboardBanners)),
|
|
173
|
+
);
|
|
174
|
+
const document = new DOMParser().parseFromString(html, "text/html");
|
|
175
|
+
const rendered = [
|
|
176
|
+
...document.querySelectorAll("[data-dms-layout-banner]"),
|
|
177
|
+
].map((node) => ({
|
|
178
|
+
key: node.getAttribute("data-dms-layout-banner"),
|
|
179
|
+
role: node.getAttribute("role"),
|
|
180
|
+
text: node.textContent,
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
expect(rendered).toEqual([
|
|
184
|
+
{ key: "billing", role: "alert", text: "t:billing.overdue" },
|
|
185
|
+
{ key: "trial", role: "status", text: "3 days left" },
|
|
186
|
+
{ key: "news", role: "status", text: "New release" },
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// The server reads the same cookie, so a dismissed banner never reaches the
|
|
191
|
+
// page only to be removed on hydration.
|
|
192
|
+
it("leaves a dismissed banner out of the server-rendered page", async () => {
|
|
193
|
+
dismissedCookie.value = ["news"];
|
|
194
|
+
serveBanners([banner({ key: "news", dismissible: true })]);
|
|
195
|
+
|
|
196
|
+
const html = await renderToString(
|
|
197
|
+
mountable(createSSRApp(DashboardBanners)),
|
|
198
|
+
);
|
|
199
|
+
expect(html).not.toContain("data-dms-layout-banners");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("offers a close button only on a dismissible banner, and closes it", async () => {
|
|
203
|
+
serveBanners([
|
|
204
|
+
banner({ key: "sticky" }),
|
|
205
|
+
banner({ key: "closable", dismissible: true }),
|
|
206
|
+
]);
|
|
207
|
+
const host = document.createElement("div");
|
|
208
|
+
const app = mountable(createApp(DashboardBanners));
|
|
209
|
+
app.mount(host);
|
|
210
|
+
|
|
211
|
+
const buttons = host.querySelectorAll("button");
|
|
212
|
+
expect(buttons).toHaveLength(1);
|
|
213
|
+
buttons[0]!.click();
|
|
214
|
+
await nextTick();
|
|
215
|
+
|
|
216
|
+
expect(dismissedCookie.value).toEqual(["closable"]);
|
|
217
|
+
expect(
|
|
218
|
+
[...host.querySelectorAll("[data-dms-layout-banner]")].map((node) =>
|
|
219
|
+
node.getAttribute("data-dms-layout-banner"),
|
|
220
|
+
),
|
|
221
|
+
).toEqual(["sticky"]);
|
|
222
|
+
app.unmount();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -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
|
});
|