@antelopejs/dms 0.4.4 → 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 +43 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js.map +1 -1
- 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 +3 -0
- package/dist/implementations/dms/page.js +90 -28
- 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/stale-session.d.ts +2 -0
- package/dist/implementations/dms/stale-session.js +16 -0
- package/dist/implementations/dms/stale-session.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +3 -1
- package/dist/implementations/dms-auth/index.js +38 -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/client-ip.d.ts +7 -0
- package/dist/routes/auth/client-ip.js +42 -0
- package/dist/routes/auth/client-ip.js.map +1 -0
- package/dist/routes/auth/index.d.ts +1 -0
- package/dist/routes/auth/index.js +1 -0
- package/dist/routes/auth/index.js.map +1 -1
- 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/routes/auth.d.ts +3 -0
- package/dist/routes/auth.js +15 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/test/integration/auth/client-ip.test.d.ts +1 -0
- package/dist/test/integration/auth/client-ip.test.js +14 -0
- package/dist/test/integration/auth/client-ip.test.js.map +1 -0
- 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/page-session.test.d.ts +1 -0
- package/dist/test/integration/page-session.test.js +52 -0
- package/dist/test/integration/page-session.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/client-ip.test.d.ts +1 -0
- package/dist/test/unit/client-ip.test.js +77 -0
- package/dist/test/unit/client-ip.test.js.map +1 -0
- 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/interfaces/dms-base/table-view-tab-count-warning.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js +96 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.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/app/middleware/auth.ts +5 -2
- 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-core/app/composables/useApiError.ts +27 -2
- package/frontend-vue/layers/dms-core/app/utils/menu.ts +26 -10
- package/frontend-vue/layers/dms-core/app/utils/routePath.ts +23 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +3 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +3 -0
- package/frontend-vue/layers/dms-core/shared/types/runtime-config.d.ts +4 -1
- package/frontend-vue/layers/dms-layout/app/app.config.ts +9 -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/components/navigation/CollapsedMenuItem.vue +0 -5
- package/frontend-vue/layers/dms-layout/app/components/profile/ProfileLanguage.vue +3 -23
- 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/composables/profile/useUserLanguage.ts +33 -0
- 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/plugins/command-palette-session.client.ts +2 -1
- 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/data-types/registerDefaults.ts +31 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationBadges.ts +36 -0
- 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/build/types/tree.ts +5 -10
- 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/api-error-title.test.ts +24 -0
- package/frontend-vue/tests/auth-middleware-error.test.ts +58 -0
- package/frontend-vue/tests/breadcrumb-target.test.ts +50 -0
- package/frontend-vue/tests/button-disabled-theme.test.ts +21 -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/menu-navigation.test.ts +39 -10
- 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/relation-badges.test.ts +32 -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/user-language.test.ts +58 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/REFERENCE.md +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { TabItem } from "./item";
|
|
2
2
|
|
|
3
|
+
// A tab set is also used directly in hand-written pages, outside any
|
|
4
|
+
// registered component: the ids exist only for server-driven ones.
|
|
3
5
|
interface TabComponentProps {
|
|
4
|
-
componentId
|
|
5
|
-
pageId
|
|
6
|
+
componentId?: string;
|
|
7
|
+
pageId?: string;
|
|
6
8
|
routeParams?: Record<string, string>;
|
|
7
9
|
watchActions?: WatchAction[];
|
|
8
10
|
childCount?: number;
|
|
@@ -80,14 +80,14 @@ export function useTab(
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
watch(activeTab, async (newTab, oldTab) => {
|
|
83
|
-
if (newTab
|
|
83
|
+
if (newTab === oldTab) return;
|
|
84
|
+
if (props.componentId) {
|
|
84
85
|
sendComponentEvent(TabEvents.TAB_CHANGE, props.componentId, {
|
|
85
86
|
previousTab: Number.parseInt(oldTab, 10),
|
|
86
87
|
currentTab: Number.parseInt(newTab, 10),
|
|
87
88
|
});
|
|
88
|
-
|
|
89
|
-
await updateUrl(newTab);
|
|
90
89
|
}
|
|
90
|
+
await updateUrl(newTab);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
watch(
|
|
@@ -3,6 +3,8 @@ import type { ActionTarget } from "./action-target";
|
|
|
3
3
|
export type CustomButtonVariant = "solid" | "outline" | "ghost" | "link";
|
|
4
4
|
|
|
5
5
|
export interface CustomButton {
|
|
6
|
+
/** Set when a quick action can press the button. */
|
|
7
|
+
id?: string;
|
|
6
8
|
label: string;
|
|
7
9
|
icon?: string;
|
|
8
10
|
variant?: CustomButtonVariant;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* How an `openForm` quick action reaches its table view: it routes
|
|
3
|
-
* action's page with these query parameters, and the table view opens
|
|
4
|
-
* creation form when it mounts. Carried by the URL rather than a window event
|
|
2
|
+
* How an `openForm` or `button` quick action reaches its table view: it routes
|
|
3
|
+
* to the action's page with these query parameters, and the table view opens
|
|
4
|
+
* its creation form, or presses the named button, when it mounts. Carried by the URL rather than a window event
|
|
5
5
|
* so the action works from any page — the listener would otherwise have to
|
|
6
6
|
* exist before the navigation that creates it.
|
|
7
7
|
*/
|
|
@@ -9,3 +9,38 @@ export const QUICK_ACTION_QUERY_KEY = "quickAction";
|
|
|
9
9
|
export const QUICK_ACTION_ADD = "add";
|
|
10
10
|
/** Which table view answers, when the page mounts more than one. */
|
|
11
11
|
export const QUICK_ACTION_COMPONENT_KEY = "quickActionComponent";
|
|
12
|
+
export const QUICK_ACTION_BUTTON = "button";
|
|
13
|
+
/** Which of the table view's custom buttons a `button` quick action presses. */
|
|
14
|
+
export const QUICK_ACTION_BUTTON_KEY = "quickActionButton";
|
|
15
|
+
|
|
16
|
+
export type QuickActionIntent =
|
|
17
|
+
| { kind: "add" }
|
|
18
|
+
| { kind: "button"; button: string };
|
|
19
|
+
|
|
20
|
+
type QuickActionQuery = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
const intentReaders: Record<
|
|
23
|
+
string,
|
|
24
|
+
(query: QuickActionQuery) => QuickActionIntent | undefined
|
|
25
|
+
> = {
|
|
26
|
+
[QUICK_ACTION_ADD]: () => ({ kind: "add" }),
|
|
27
|
+
[QUICK_ACTION_BUTTON]: (query) => {
|
|
28
|
+
const button = query[QUICK_ACTION_BUTTON_KEY];
|
|
29
|
+
return typeof button === "string" ? { kind: "button", button } : undefined;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What the route asks the table view `componentId` to run. A page may mount
|
|
35
|
+
* several table views, so the quick action always names the one it means —
|
|
36
|
+
* resolved server-side — and only that one answers.
|
|
37
|
+
*/
|
|
38
|
+
export function readQuickActionIntent(
|
|
39
|
+
query: QuickActionQuery,
|
|
40
|
+
componentId: string | undefined,
|
|
41
|
+
): QuickActionIntent | undefined {
|
|
42
|
+
if (query[QUICK_ACTION_COMPONENT_KEY] !== componentId) return undefined;
|
|
43
|
+
const action = query[QUICK_ACTION_QUERY_KEY];
|
|
44
|
+
if (typeof action !== "string") return undefined;
|
|
45
|
+
return intentReaders[action]?.(query);
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createSSRApp, defineComponent, h } from "vue";
|
|
3
|
+
import { renderToString } from "vue/server-renderer";
|
|
4
|
+
import { createI18n } from "vue-i18n";
|
|
5
|
+
import AdminInvite from "../layers/dms-layout/app/emails/EmailAdminInvite.vue";
|
|
6
|
+
import en from "../layers/dms-layout/i18n/locales/layout-en-GB.json";
|
|
7
|
+
import fr from "../layers/dms-layout/i18n/locales/layout-fr-FR.json";
|
|
8
|
+
|
|
9
|
+
const EMAIL_ELEMENTS = ["EHtml", "EHead", "EBody", "EContainer", "ESection"];
|
|
10
|
+
const LEAF_ELEMENTS = ["EText", "EHeading", "EButton"];
|
|
11
|
+
|
|
12
|
+
function passthrough(tag: string) {
|
|
13
|
+
return defineComponent({
|
|
14
|
+
setup:
|
|
15
|
+
(_, { slots }) =>
|
|
16
|
+
() =>
|
|
17
|
+
h(tag, slots.default?.()),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function renderInvite(
|
|
22
|
+
props: Record<string, unknown>,
|
|
23
|
+
locale = "en",
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
vi.stubGlobal("useDmsAppConfig", () => ({}));
|
|
26
|
+
vi.stubGlobal("useDmsRuntimeConfig", () => ({ public: {} }));
|
|
27
|
+
const app = createSSRApp(AdminInvite, {
|
|
28
|
+
email: "invitee@local.test",
|
|
29
|
+
signupLink: "https://app.local/auth/signup?token=t",
|
|
30
|
+
expiresInDays: 7,
|
|
31
|
+
...props,
|
|
32
|
+
});
|
|
33
|
+
app.use(
|
|
34
|
+
createI18n({
|
|
35
|
+
legacy: false,
|
|
36
|
+
locale,
|
|
37
|
+
fallbackLocale: "en",
|
|
38
|
+
messages: { en, fr },
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
EMAIL_ELEMENTS.forEach((name) => app.component(name, passthrough("div")));
|
|
42
|
+
LEAF_ELEMENTS.forEach((name) => app.component(name, passthrough("p")));
|
|
43
|
+
app.component("EImg", passthrough("img"));
|
|
44
|
+
app.component("EHr", passthrough("hr"));
|
|
45
|
+
const html = await renderToString(app);
|
|
46
|
+
vi.unstubAllGlobals();
|
|
47
|
+
return html.replace(/<!--[^>]*-->/g, "").replace(/'/g, "'");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("EmailAdminInvite", () => {
|
|
51
|
+
it("keeps today's generic wording without any name", async () => {
|
|
52
|
+
const html = await renderInvite({});
|
|
53
|
+
expect(html).toContain("You're Invited!");
|
|
54
|
+
expect(html).toContain(
|
|
55
|
+
"You've been invited to join our platform. Click the button below to create your account:",
|
|
56
|
+
);
|
|
57
|
+
expect(html).toContain("This invitation expires in 7 days.");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("names the workspace, the platform and the inviter", async () => {
|
|
61
|
+
const html = await renderInvite({
|
|
62
|
+
workspaceName: "Storefront",
|
|
63
|
+
platformName: "Acme",
|
|
64
|
+
inviterName: "Ada",
|
|
65
|
+
});
|
|
66
|
+
expect(html).toContain("You're invited to Storefront");
|
|
67
|
+
expect(html).toContain("Ada invited you to join Storefront on Acme.");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("names the platform when the workspace is unknown", async () => {
|
|
71
|
+
const html = await renderInvite({ platformName: "Acme" });
|
|
72
|
+
expect(html).toContain("You've been invited to join Acme.");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("writes the email in French", async () => {
|
|
76
|
+
const html = await renderInvite(
|
|
77
|
+
{ workspaceName: "Storefront", platformName: "Acme" },
|
|
78
|
+
"fr",
|
|
79
|
+
);
|
|
80
|
+
expect(html).toContain("Vous êtes invité à rejoindre Storefront");
|
|
81
|
+
expect(html).toContain(
|
|
82
|
+
"Vous avez été invité à rejoindre Storefront sur Acme.",
|
|
83
|
+
);
|
|
84
|
+
expect(html).toContain("Cette invitation expire dans 7 jours.");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveApiErrorTitle } from "../layers/dms-core/app/composables/useApiError";
|
|
3
|
+
|
|
4
|
+
describe("resolveApiErrorTitle", () => {
|
|
5
|
+
it("titles a 4xx response as a refused request", () => {
|
|
6
|
+
expect(resolveApiErrorTitle({ statusCode: 403 })).toBe(
|
|
7
|
+
"error.request_refused.title",
|
|
8
|
+
);
|
|
9
|
+
expect(resolveApiErrorTitle({ status: 422 })).toBe(
|
|
10
|
+
"error.request_refused.title",
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("titles a 5xx response as a server error", () => {
|
|
15
|
+
expect(resolveApiErrorTitle({ statusCode: 502 })).toBe("error.500.title");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("titles an error without a status as a server error", () => {
|
|
19
|
+
expect(resolveApiErrorTitle(new Error("network down"))).toBe(
|
|
20
|
+
"error.500.title",
|
|
21
|
+
);
|
|
22
|
+
expect(resolveApiErrorTitle(undefined)).toBe("error.500.title");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
type Middleware = (to: unknown) => Promise<unknown>;
|
|
4
|
+
|
|
5
|
+
const pageError = { value: null as { statusCode: number } | null };
|
|
6
|
+
const navigateDms = vi.fn();
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.resetModules();
|
|
10
|
+
vi.stubGlobal("defineDmsMiddleware", (handler: Middleware) => handler);
|
|
11
|
+
vi.stubGlobal("useError", () => pageError);
|
|
12
|
+
vi.stubGlobal("abortNavigation", () => false);
|
|
13
|
+
vi.stubGlobal("navigateDms", navigateDms);
|
|
14
|
+
vi.stubGlobal("useSiteLayout", () => ({
|
|
15
|
+
siteLayout: { value: {} },
|
|
16
|
+
findMatchingRoute: () => ({ metadata: {} }),
|
|
17
|
+
}));
|
|
18
|
+
vi.stubGlobal("useUserSession", () => ({ loggedIn: { value: false } }));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
pageError.value = null;
|
|
23
|
+
navigateDms.mockReset();
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function loadMiddleware(): Promise<Middleware> {
|
|
28
|
+
const module = await import("../layers/dms-auth/app/middleware/auth");
|
|
29
|
+
return module.default as unknown as Middleware;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const privateRoute = {
|
|
33
|
+
path: "/projects/project",
|
|
34
|
+
fullPath: "/projects/project",
|
|
35
|
+
matched: [{}],
|
|
36
|
+
meta: { auth: true },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("auth middleware", () => {
|
|
40
|
+
it("aborts without throwing when the page already raised an error", async () => {
|
|
41
|
+
pageError.value = { statusCode: 404 };
|
|
42
|
+
const middleware = await loadMiddleware();
|
|
43
|
+
|
|
44
|
+
await expect(middleware(privateRoute)).resolves.toBe(false);
|
|
45
|
+
expect(navigateDms).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("still sends a signed-out visitor to the login page", async () => {
|
|
49
|
+
const middleware = await loadMiddleware();
|
|
50
|
+
|
|
51
|
+
await middleware(privateRoute);
|
|
52
|
+
|
|
53
|
+
expect(navigateDms).toHaveBeenCalledWith({
|
|
54
|
+
path: "/auth",
|
|
55
|
+
query: { redirect: "/projects/project" },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildBreadcrumbTarget } from "../layers/dms-layout/app/utils/breadcrumb";
|
|
3
|
+
|
|
4
|
+
const PAGE = { layoutUrl: "/projects/project/pagelayout" };
|
|
5
|
+
const PROJECT_PAGE = {
|
|
6
|
+
...PAGE,
|
|
7
|
+
validation: { requiredQueryParams: ["project"] },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("buildBreadcrumbTarget", () => {
|
|
11
|
+
it("links a page without query requirements to its bare path", () => {
|
|
12
|
+
expect(
|
|
13
|
+
buildBreadcrumbTarget("/projects", PAGE, { project: "p1" }),
|
|
14
|
+
).to.equal("/projects");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("gives a category without a page no link", () => {
|
|
18
|
+
expect(buildBreadcrumbTarget("/projects", {}, {})).to.equal(undefined);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("carries only the query values an ancestor page requires", () => {
|
|
22
|
+
expect(
|
|
23
|
+
buildBreadcrumbTarget("/projects/project", PROJECT_PAGE, {
|
|
24
|
+
project: "p 1&x",
|
|
25
|
+
service: "s1",
|
|
26
|
+
}),
|
|
27
|
+
).to.equal("/projects/project?project=p+1%26x");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("gives no link when a required query value is missing", () => {
|
|
31
|
+
expect(
|
|
32
|
+
buildBreadcrumbTarget("/projects/project", PROJECT_PAGE, {
|
|
33
|
+
service: "s1",
|
|
34
|
+
}),
|
|
35
|
+
).to.equal(undefined);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("gives no link when a required query value is empty or repeated", () => {
|
|
39
|
+
expect(
|
|
40
|
+
buildBreadcrumbTarget("/projects/project", PROJECT_PAGE, {
|
|
41
|
+
project: "",
|
|
42
|
+
}),
|
|
43
|
+
).to.equal(undefined);
|
|
44
|
+
expect(
|
|
45
|
+
buildBreadcrumbTarget("/projects/project", PROJECT_PAGE, {
|
|
46
|
+
project: ["a", "b"],
|
|
47
|
+
}),
|
|
48
|
+
).to.equal(undefined);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
import appConfig from "../layers/dms-layout/app/app.config";
|
|
4
|
+
|
|
5
|
+
// Nuxt UI 4's own button base, which the app config is merged over.
|
|
6
|
+
const NUXT_UI_BUTTON_BASE =
|
|
7
|
+
"rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75";
|
|
8
|
+
|
|
9
|
+
describe("button theme", () => {
|
|
10
|
+
const merged = twMerge(NUXT_UI_BUTTON_BASE, appConfig.ui.button.slots.base);
|
|
11
|
+
|
|
12
|
+
it("fades disabled buttons past Nuxt UI's default", () => {
|
|
13
|
+
expect(merged).toContain("disabled:opacity-50");
|
|
14
|
+
expect(merged).toContain("aria-disabled:opacity-50");
|
|
15
|
+
expect(merged).not.toContain("opacity-75");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("cancels the press effect on a disabled button", () => {
|
|
19
|
+
expect(merged).toContain("disabled:active:scale-100");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createSSRApp, defineComponent, h, ref, type PropType } from "vue";
|
|
2
|
+
import { renderToString } from "vue/server-renderer";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import ChartCard from "../layers/dms-ui/app/components/chart/ChartCard.vue";
|
|
5
|
+
import Chart from "../layers/dms-ui/app/components/chart/Chart.vue";
|
|
6
|
+
import type { ChartType } from "../layers/dms-ui/app/composables/chart/types";
|
|
7
|
+
|
|
8
|
+
const CARD_SERIES = [
|
|
9
|
+
{
|
|
10
|
+
name: "Plans",
|
|
11
|
+
data: [
|
|
12
|
+
{ x: "Starter", y: 3 },
|
|
13
|
+
{ x: "Pro", y: 5, label: "Pro plan" },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
19
|
+
useChartFetch: (options: { fetchUrl?: string }) => ({
|
|
20
|
+
data: ref(options.fetchUrl ? { value: 8, series: CARD_SERIES } : null),
|
|
21
|
+
isLoading: ref(false),
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
interface CircularOptions {
|
|
26
|
+
labels?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PlotProbe = defineComponent({
|
|
30
|
+
props: {
|
|
31
|
+
options: { type: Object as PropType<CircularOptions>, required: true },
|
|
32
|
+
series: { type: Array as PropType<unknown[]>, required: true },
|
|
33
|
+
},
|
|
34
|
+
setup: (props) => () =>
|
|
35
|
+
h("output", [
|
|
36
|
+
h("span", { class: "labels" }, JSON.stringify(props.options.labels)),
|
|
37
|
+
h("span", { class: "series" }, JSON.stringify(props.series)),
|
|
38
|
+
]),
|
|
39
|
+
});
|
|
40
|
+
const Slot = defineComponent({
|
|
41
|
+
setup:
|
|
42
|
+
(_, { slots }) =>
|
|
43
|
+
() =>
|
|
44
|
+
h("div", slots.default?.()),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const SLOT_COMPONENTS = [
|
|
48
|
+
"DmsCard",
|
|
49
|
+
"DmsClientOnly",
|
|
50
|
+
"UIcon",
|
|
51
|
+
"USkeleton",
|
|
52
|
+
"DmsTrendBadge",
|
|
53
|
+
"DmsSparkline",
|
|
54
|
+
"UBadge",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
async function renderNested(type: ChartType) {
|
|
58
|
+
const app = createSSRApp({
|
|
59
|
+
render: () =>
|
|
60
|
+
h(
|
|
61
|
+
ChartCard,
|
|
62
|
+
{
|
|
63
|
+
title: "Plans",
|
|
64
|
+
componentId: "card",
|
|
65
|
+
pageId: "donut",
|
|
66
|
+
fetchUrl: "/plans",
|
|
67
|
+
},
|
|
68
|
+
() => h(Chart, { type, componentId: "plot", pageId: "donut" }),
|
|
69
|
+
),
|
|
70
|
+
});
|
|
71
|
+
for (const name of SLOT_COMPONENTS) app.component(name, Slot);
|
|
72
|
+
app.component("DmsApexChartHost", PlotProbe);
|
|
73
|
+
return renderToString(app);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
vi.stubGlobal("useI18n", () => ({ locale: ref("en-US"), t: String }));
|
|
78
|
+
vi.stubGlobal("useTranslation", () => ({ processI18n: String }));
|
|
79
|
+
vi.stubGlobal("useComponentEvent", () => ({}));
|
|
80
|
+
vi.stubGlobal("useWatch", () => ({ state: ref({}) }));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
84
|
+
|
|
85
|
+
describe("circular chart nested in a ChartCard", () => {
|
|
86
|
+
it.each<ChartType>(["donut", "pie"])(
|
|
87
|
+
"draws the card's first series as %s slices",
|
|
88
|
+
async (type) => {
|
|
89
|
+
const html = await renderNested(type);
|
|
90
|
+
expect(html).not.toContain("dms.chart.no_data");
|
|
91
|
+
expect(html).toContain(
|
|
92
|
+
'class="labels">["Starter","Pro plan"]',
|
|
93
|
+
);
|
|
94
|
+
expect(html).toContain(`class="series">[3,5]`);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
createApp,
|
|
6
|
+
defineComponent,
|
|
7
|
+
h,
|
|
8
|
+
onMounted,
|
|
9
|
+
ref,
|
|
10
|
+
type App,
|
|
11
|
+
} from "vue";
|
|
12
|
+
import ErrorPage from "../layers/dms-layout/app/error.vue";
|
|
13
|
+
import {
|
|
14
|
+
HTTP_FORBIDDEN,
|
|
15
|
+
HTTP_NOT_FOUND,
|
|
16
|
+
HTTP_UNAUTHORIZED,
|
|
17
|
+
} from "../layers/dms-core/app/utils/http-status";
|
|
18
|
+
|
|
19
|
+
const TECHNICAL_MESSAGE = "DMS backend request failed";
|
|
20
|
+
|
|
21
|
+
let app: App;
|
|
22
|
+
let host: HTMLDivElement;
|
|
23
|
+
|
|
24
|
+
const Passthrough = defineComponent({
|
|
25
|
+
setup:
|
|
26
|
+
(_, { slots }) =>
|
|
27
|
+
() =>
|
|
28
|
+
h("div", slots.default?.()),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function installRuntime() {
|
|
32
|
+
Object.entries({
|
|
33
|
+
computed,
|
|
34
|
+
ref,
|
|
35
|
+
onMounted,
|
|
36
|
+
HTTP_FORBIDDEN,
|
|
37
|
+
HTTP_NOT_FOUND,
|
|
38
|
+
HTTP_UNAUTHORIZED,
|
|
39
|
+
}).forEach(([key, value]) => vi.stubGlobal(key, value));
|
|
40
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
41
|
+
vi.stubGlobal("useDmsRouter", () => ({ back: vi.fn() }));
|
|
42
|
+
vi.stubGlobal("useHomepage", () => "/");
|
|
43
|
+
vi.stubGlobal("useSessionRecovery", () => ({
|
|
44
|
+
loggedIn: ref(true),
|
|
45
|
+
reconcileSession: vi.fn(),
|
|
46
|
+
redirectToAuth: vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mountError(statusCode: number) {
|
|
51
|
+
app = createApp(ErrorPage, {
|
|
52
|
+
error: { statusCode, message: TECHNICAL_MESSAGE },
|
|
53
|
+
});
|
|
54
|
+
app.config.globalProperties.$t = (key: string) => key;
|
|
55
|
+
app.component("DmsAppLogo", Passthrough);
|
|
56
|
+
app.component("DmsCard", Passthrough);
|
|
57
|
+
app.component("Icon", Passthrough);
|
|
58
|
+
app.component("UButton", Passthrough);
|
|
59
|
+
app.mount(host);
|
|
60
|
+
return host.textContent ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
installRuntime();
|
|
65
|
+
host = document.createElement("div");
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
app?.unmount();
|
|
69
|
+
vi.unstubAllGlobals();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it.each([HTTP_NOT_FOUND, HTTP_FORBIDDEN])(
|
|
73
|
+
"keeps the technical cause off the %i card",
|
|
74
|
+
(statusCode) => {
|
|
75
|
+
const text = mountError(statusCode);
|
|
76
|
+
expect(text).toContain(`error.${statusCode}.description`);
|
|
77
|
+
expect(text).not.toContain(TECHNICAL_MESSAGE);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it("still shows the message of an unexpected error", () => {
|
|
82
|
+
expect(mountError(500)).toContain(TECHNICAL_MESSAGE);
|
|
83
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildFavoritePage,
|
|
4
|
+
isFavoritePathValid,
|
|
5
|
+
} from "../layers/dms-layout/app/utils/favorites";
|
|
6
|
+
|
|
7
|
+
const translate = (text: string) =>
|
|
8
|
+
text.startsWith("$") ? `t(${text.slice(1)})` : text;
|
|
9
|
+
|
|
10
|
+
const SETTINGS_PAGE = {
|
|
11
|
+
id: "settings",
|
|
12
|
+
displayName: "$page.settings.title",
|
|
13
|
+
icon: "i-ph-gear",
|
|
14
|
+
};
|
|
15
|
+
const PROJECT_PAGE = {
|
|
16
|
+
id: "project",
|
|
17
|
+
displayName: "$cloud.project.title",
|
|
18
|
+
icon: "i-ph-cube",
|
|
19
|
+
validation: { requiredQueryParams: ["project"] },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ROUTES: Record<string, typeof SETTINGS_PAGE | typeof PROJECT_PAGE> = {
|
|
23
|
+
"/settings": SETTINGS_PAGE,
|
|
24
|
+
"/projects/project": PROJECT_PAGE,
|
|
25
|
+
};
|
|
26
|
+
const resolveRoute = (path: string) =>
|
|
27
|
+
ROUTES[path] ? { metadata: ROUTES[path] } : null;
|
|
28
|
+
|
|
29
|
+
describe("buildFavoritePage", () => {
|
|
30
|
+
it("keeps a plain page on its path and its translatable display name", () => {
|
|
31
|
+
expect(
|
|
32
|
+
buildFavoritePage({
|
|
33
|
+
path: "/settings",
|
|
34
|
+
metadata: SETTINGS_PAGE,
|
|
35
|
+
query: { tab: "general" },
|
|
36
|
+
translate,
|
|
37
|
+
}),
|
|
38
|
+
).toEqual({
|
|
39
|
+
id: "settings",
|
|
40
|
+
path: "/settings",
|
|
41
|
+
title: "$page.settings.title",
|
|
42
|
+
icon: "i-ph-gear",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps the query a page requires and names it after the page and its values", () => {
|
|
47
|
+
expect(
|
|
48
|
+
buildFavoritePage({
|
|
49
|
+
path: "/projects/project",
|
|
50
|
+
metadata: PROJECT_PAGE,
|
|
51
|
+
query: { project: "p 1", service: "s1" },
|
|
52
|
+
translate,
|
|
53
|
+
}),
|
|
54
|
+
).toEqual({
|
|
55
|
+
id: "/projects/project?project=p+1",
|
|
56
|
+
path: "/projects/project?project=p+1",
|
|
57
|
+
title: "t(cloud.project.title) · p 1",
|
|
58
|
+
icon: "i-ph-cube",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("joins every required query value in the title", () => {
|
|
63
|
+
expect(
|
|
64
|
+
buildFavoritePage({
|
|
65
|
+
path: "/projects/project/service",
|
|
66
|
+
metadata: {
|
|
67
|
+
...PROJECT_PAGE,
|
|
68
|
+
validation: { requiredQueryParams: ["project", "service"] },
|
|
69
|
+
},
|
|
70
|
+
query: { project: "p1", service: "s1" },
|
|
71
|
+
translate,
|
|
72
|
+
})?.title,
|
|
73
|
+
).toBe("t(cloud.project.title) · p1 · s1");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gives no favorite when a required query value is missing", () => {
|
|
77
|
+
expect(
|
|
78
|
+
buildFavoritePage({
|
|
79
|
+
path: "/projects/project",
|
|
80
|
+
metadata: PROJECT_PAGE,
|
|
81
|
+
query: {},
|
|
82
|
+
translate,
|
|
83
|
+
}),
|
|
84
|
+
).toBe(null);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("isFavoritePathValid", () => {
|
|
89
|
+
it("accepts favorites of plain pages, with or without a query", () => {
|
|
90
|
+
expect(isFavoritePathValid("/settings", resolveRoute)).toBe(true);
|
|
91
|
+
expect(isFavoritePathValid("/settings?tab=x", resolveRoute)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("accepts a favorite carrying the query its page requires", () => {
|
|
95
|
+
expect(
|
|
96
|
+
isFavoritePathValid("/projects/project?project=p+1", resolveRoute),
|
|
97
|
+
).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("drops favorites that match no page or lack a required query value", () => {
|
|
101
|
+
expect(isFavoritePathValid("/unknown", resolveRoute)).toBe(false);
|
|
102
|
+
expect(isFavoritePathValid("/projects/project", resolveRoute)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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 ICON_TAG = /<(?:UIcon|Icon)\b[^>]*?>/gs;
|
|
6
|
+
const STATIC_ARIA_HIDDEN = /(?<![:\w-])aria-hidden=/;
|
|
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("icon aria-hidden bindings", () => {
|
|
16
|
+
// The icon hands its attributes to Iconify's component, whose `ariaHidden`
|
|
17
|
+
// prop is a Boolean: a static attribute reaches it as the string "true".
|
|
18
|
+
it("binds aria-hidden on icons as a boolean", () => {
|
|
19
|
+
const offenders = walk(join(process.cwd(), "layers"))
|
|
20
|
+
.filter((path) => path.endsWith(".vue"))
|
|
21
|
+
.flatMap((path) =>
|
|
22
|
+
[...readFileSync(path, "utf8").matchAll(ICON_TAG)]
|
|
23
|
+
.filter(([tag]) => STATIC_ARIA_HIDDEN.test(tag))
|
|
24
|
+
.map(() => relative(process.cwd(), path)),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(offenders).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
});
|