@antelopejs/dms 0.7.8 → 0.7.10
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 +45 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +25 -0
- package/dist/test/attachment-host/capabilities.js +53 -1
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +13 -0
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Guard reads a route and, when its condition holds (the value it names is
|
|
3
|
+
* empty, or the route answers 404), draws one EmptyState instead of its
|
|
4
|
+
* children; it keeps them while the route loads, after any other failure,
|
|
5
|
+
* and once the value is set.
|
|
6
|
+
*/
|
|
7
|
+
import { computed, createSSRApp, defineComponent, h, ref } from "vue";
|
|
8
|
+
import { renderToString } from "vue/server-renderer";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import Guard from "../layers/dms-ui/app/components/guard/Guard.vue";
|
|
11
|
+
import {
|
|
12
|
+
isEmptyValue,
|
|
13
|
+
readFieldPath,
|
|
14
|
+
} from "../layers/dms-ui/app/build/composables/blocks/useGuard";
|
|
15
|
+
|
|
16
|
+
const answer = ref<unknown>(null);
|
|
17
|
+
const failure = ref<unknown>(null);
|
|
18
|
+
const fetchOptions = vi.fn();
|
|
19
|
+
|
|
20
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
21
|
+
useChartFetch: (options: unknown) => {
|
|
22
|
+
fetchOptions(options);
|
|
23
|
+
return { data: answer, isLoading: ref(false), error: failure };
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("../layers/dms-core/app/composables/watch/useWatch", () => ({
|
|
28
|
+
useWatch: () => ({ state: ref({}) }),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const Slot = defineComponent({
|
|
32
|
+
setup:
|
|
33
|
+
(_, { slots }) =>
|
|
34
|
+
() =>
|
|
35
|
+
h("div", slots.default?.()),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const Button = defineComponent({
|
|
39
|
+
props: { label: String, to: String },
|
|
40
|
+
setup: (props) => () =>
|
|
41
|
+
h("a", { "data-button": "", href: props.to }, props.label),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const GUARD = {
|
|
45
|
+
fetchUrl: "/api/routes/activity?route={{query.route}}",
|
|
46
|
+
periodScope: "api",
|
|
47
|
+
realtimeTopic: "requests",
|
|
48
|
+
field: "summary.calls",
|
|
49
|
+
fallback: {
|
|
50
|
+
title: "$api.routes.statistics.idle_title",
|
|
51
|
+
icon: "i-ph-moon-stars",
|
|
52
|
+
actions: [{ label: "Test this route", to: "/routes?tab=2" }],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
async function render(props: Record<string, unknown>): Promise<string> {
|
|
57
|
+
const app = createSSRApp({
|
|
58
|
+
render: () =>
|
|
59
|
+
h(
|
|
60
|
+
Guard,
|
|
61
|
+
{ componentId: "statistics", pageId: "routes", ...props },
|
|
62
|
+
{ default: () => h("section", { "data-children": "" }, "children") },
|
|
63
|
+
),
|
|
64
|
+
});
|
|
65
|
+
for (const name of ["UIcon", "DmsLink"]) app.component(name, Slot);
|
|
66
|
+
app.component("UButton", Button);
|
|
67
|
+
return renderToString(app);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
answer.value = null;
|
|
72
|
+
failure.value = null;
|
|
73
|
+
fetchOptions.mockClear();
|
|
74
|
+
// Nuxt auto-imports, which the components use unimported.
|
|
75
|
+
vi.stubGlobal("ref", ref);
|
|
76
|
+
vi.stubGlobal("computed", computed);
|
|
77
|
+
vi.stubGlobal("useI18n", () => ({
|
|
78
|
+
locale: ref("en-US"),
|
|
79
|
+
t: (key: string) => `t(${key})`,
|
|
80
|
+
}));
|
|
81
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
82
|
+
processI18n: (text: string) => (text.startsWith("$") ? `t(${text})` : text),
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
87
|
+
|
|
88
|
+
describe("Guard", () => {
|
|
89
|
+
it("reads the route like a block's data source", async () => {
|
|
90
|
+
await render({ ...GUARD });
|
|
91
|
+
expect(fetchOptions).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
fetchUrl: GUARD.fetchUrl,
|
|
94
|
+
periodScope: "api",
|
|
95
|
+
realtimeTopic: "requests",
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps its children until the route answers", async () => {
|
|
101
|
+
const html = await render({ ...GUARD });
|
|
102
|
+
expect(html).toContain("data-children");
|
|
103
|
+
expect(html).not.toContain("idle_title");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("keeps its children while the value is set", async () => {
|
|
107
|
+
answer.value = { summary: { calls: 12 } };
|
|
108
|
+
const html = await render({ ...GUARD });
|
|
109
|
+
expect(html).toContain("data-children");
|
|
110
|
+
expect(html).not.toContain("idle_title");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("keeps its children after a failure", async () => {
|
|
114
|
+
answer.value = { summary: { calls: 0 } };
|
|
115
|
+
failure.value = { statusCode: 500 };
|
|
116
|
+
const html = await render({ ...GUARD });
|
|
117
|
+
expect(html).toContain("data-children");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("swaps its children for the fallback when the value is empty", async () => {
|
|
121
|
+
answer.value = { summary: { calls: 0 } };
|
|
122
|
+
const html = await render({ ...GUARD });
|
|
123
|
+
expect(html).not.toContain("data-children");
|
|
124
|
+
expect(html).toContain("t($api.routes.statistics.idle_title)");
|
|
125
|
+
expect(html).toMatch(
|
|
126
|
+
/<a data-button href="\/routes\?tab=2"[^>]*>Test this route</,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("checks the whole answer without a field", async () => {
|
|
131
|
+
answer.value = [];
|
|
132
|
+
const html = await render({ ...GUARD, field: undefined });
|
|
133
|
+
expect(html).not.toContain("data-children");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("lays the answer's own emptyState over the fallback", async () => {
|
|
137
|
+
answer.value = {
|
|
138
|
+
summary: { calls: 0 },
|
|
139
|
+
emptyState: {
|
|
140
|
+
description: "Last called Oct 10",
|
|
141
|
+
actions: [{ label: "Test this route", to: "/routes?route=r1&tab=2" }],
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const html = await render({ ...GUARD });
|
|
145
|
+
expect(html).toContain("t($api.routes.statistics.idle_title)");
|
|
146
|
+
expect(html).toContain("Last called Oct 10");
|
|
147
|
+
expect(html).toMatch(
|
|
148
|
+
/<a data-button href="\/routes\?route=r1&tab=2"[^>]*>/,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("falls back on a translated title without a fallback", async () => {
|
|
153
|
+
answer.value = { summary: { calls: 0 } };
|
|
154
|
+
const html = await render({ ...GUARD, fallback: undefined });
|
|
155
|
+
expect(html).toContain("t(dms.blocks.empty)");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("when notFound", () => {
|
|
159
|
+
const NOT_FOUND = {
|
|
160
|
+
fetchUrl: "/api/routes/{{params.id}}",
|
|
161
|
+
when: "notFound",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
it("swaps its children for Not found when the route answers 404", async () => {
|
|
165
|
+
failure.value = { statusCode: 404 };
|
|
166
|
+
const html = await render(NOT_FOUND);
|
|
167
|
+
expect(html).not.toContain("data-children");
|
|
168
|
+
expect(html).toContain("t(dms.blocks.not_found)");
|
|
169
|
+
expect(html).toContain("t(dms.blocks.not_found_hint)");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("describes it with the 404's message, as a block does", async () => {
|
|
173
|
+
failure.value = {
|
|
174
|
+
statusCode: 404,
|
|
175
|
+
data: { message: "No such route" },
|
|
176
|
+
};
|
|
177
|
+
const html = await render(NOT_FOUND);
|
|
178
|
+
expect(html).toContain("t(dms.blocks.not_found)");
|
|
179
|
+
expect(html).toContain("No such route");
|
|
180
|
+
expect(html).not.toContain("t(dms.blocks.not_found_hint)");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("reads the 404 body ofetch keeps on the response", async () => {
|
|
184
|
+
failure.value = {
|
|
185
|
+
response: {
|
|
186
|
+
status: 404,
|
|
187
|
+
_data: { emptyState: { title: "Workspace gone" } },
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
expect(await render(NOT_FOUND)).toContain("Workspace gone");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("lets the fallback replace the 404's message", async () => {
|
|
194
|
+
failure.value = { statusCode: 404, data: { message: "No such route" } };
|
|
195
|
+
const html = await render({
|
|
196
|
+
...NOT_FOUND,
|
|
197
|
+
fallback: { description: "This route was removed." },
|
|
198
|
+
});
|
|
199
|
+
expect(html).toContain("This route was removed.");
|
|
200
|
+
expect(html).not.toContain("No such route");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("lays the 404 body's emptyState over the fallback", async () => {
|
|
204
|
+
failure.value = {
|
|
205
|
+
statusCode: 404,
|
|
206
|
+
data: { emptyState: { description: "Deleted on Oct 10" } },
|
|
207
|
+
};
|
|
208
|
+
const html = await render({
|
|
209
|
+
...NOT_FOUND,
|
|
210
|
+
fallback: { title: "Route gone" },
|
|
211
|
+
});
|
|
212
|
+
expect(html).toContain("Route gone");
|
|
213
|
+
expect(html).toContain("Deleted on Oct 10");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("keeps its children on an answer or another failure", async () => {
|
|
217
|
+
answer.value = null;
|
|
218
|
+
expect(await render(NOT_FOUND)).toContain("data-children");
|
|
219
|
+
answer.value = { id: "r1" };
|
|
220
|
+
expect(await render(NOT_FOUND)).toContain("data-children");
|
|
221
|
+
failure.value = { statusCode: 500 };
|
|
222
|
+
expect(await render(NOT_FOUND)).toContain("data-children");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("Guard field", () => {
|
|
228
|
+
it("reads a dot path, stopping where it ends", () => {
|
|
229
|
+
expect(readFieldPath({ a: { b: 3 } }, "a.b")).toBe(3);
|
|
230
|
+
expect(readFieldPath({ a: null }, "a.b")).toBeUndefined();
|
|
231
|
+
expect(readFieldPath({ calls: 0 }, "calls")).toBe(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("calls absent, null, false, 0, empty text and empty lists empty", () => {
|
|
235
|
+
for (const value of [undefined, null, false, 0, "", []]) {
|
|
236
|
+
expect(isEmptyValue(value)).toBe(true);
|
|
237
|
+
}
|
|
238
|
+
for (const value of [1, true, "x", [0], {}]) {
|
|
239
|
+
expect(isEmptyValue(value)).toBe(false);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A page header's buttons follow the page URL: a target URL takes the
|
|
4
|
+
* `{{params.X}}` / `{{query.X}}` tokens of a block's `fetchUrl`, and a
|
|
5
|
+
* button whose availability reads the query is asked for again when the
|
|
6
|
+
* query changes.
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
computed,
|
|
11
|
+
createApp,
|
|
12
|
+
defineComponent,
|
|
13
|
+
h,
|
|
14
|
+
nextTick,
|
|
15
|
+
reactive,
|
|
16
|
+
ref,
|
|
17
|
+
type App,
|
|
18
|
+
type FunctionalComponent,
|
|
19
|
+
} from "vue";
|
|
20
|
+
import { resolveLinkVariables } from "../layers/dms-ui/app/build/utils/urlVariables";
|
|
21
|
+
import {
|
|
22
|
+
isSamePageQuery,
|
|
23
|
+
layoutUrlWithQuery,
|
|
24
|
+
pageQueryValues,
|
|
25
|
+
} from "../layers/dms-layout/app/utils/pageQuery";
|
|
26
|
+
|
|
27
|
+
const handleCustomButton = vi.fn();
|
|
28
|
+
vi.mock("#dms-ui/app/build/composables/actions/useActionTargets", () => ({
|
|
29
|
+
useActionTargets: () => ({ handleCustomButton }),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("#dms-core/app/build/composables/auth/usePermissionPreview", () => ({
|
|
32
|
+
usePermissionPreview: () => ({
|
|
33
|
+
isHeaderActionHidden: () => false,
|
|
34
|
+
session: ref(null),
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const LAYOUT_URL = "/modules/lang/queue/pagelayout";
|
|
39
|
+
const route = reactive<{ path: string; query: Record<string, unknown> }>({
|
|
40
|
+
path: "/modules/lang/queue/de",
|
|
41
|
+
query: {},
|
|
42
|
+
});
|
|
43
|
+
const authFetch = vi.fn();
|
|
44
|
+
|
|
45
|
+
const ButtonStub = defineComponent({
|
|
46
|
+
props: { label: String, disabled: Boolean, onClick: Function, to: String },
|
|
47
|
+
setup: (props) => () =>
|
|
48
|
+
h(
|
|
49
|
+
"button",
|
|
50
|
+
{
|
|
51
|
+
disabled: props.disabled,
|
|
52
|
+
"data-to": props.to,
|
|
53
|
+
onClick: () => props.onClick?.(),
|
|
54
|
+
},
|
|
55
|
+
props.label,
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
const SlotStub: FunctionalComponent = (_, { slots }) => slots.default?.();
|
|
59
|
+
|
|
60
|
+
let app: App | undefined;
|
|
61
|
+
let host: HTMLDivElement;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
route.query = { workspace: "web" };
|
|
65
|
+
handleCustomButton.mockClear();
|
|
66
|
+
authFetch.mockReset();
|
|
67
|
+
vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
|
|
68
|
+
vi.stubGlobal("computed", computed);
|
|
69
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
70
|
+
processI18n: (value: string) => value,
|
|
71
|
+
}));
|
|
72
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
73
|
+
vi.stubGlobal("useDmsRoute", () => route);
|
|
74
|
+
vi.stubGlobal("useDmsRouter", () => ({ replace: vi.fn() }));
|
|
75
|
+
vi.stubGlobal("useSiteLayout", () => ({
|
|
76
|
+
quickActions: ref(null),
|
|
77
|
+
findMatchingRoute: () => ({
|
|
78
|
+
metadata: { layoutUrl: LAYOUT_URL },
|
|
79
|
+
params: { locale: "de" },
|
|
80
|
+
}),
|
|
81
|
+
}));
|
|
82
|
+
host = document.createElement("div");
|
|
83
|
+
document.body.append(host);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
app?.unmount();
|
|
88
|
+
app = undefined;
|
|
89
|
+
host.remove();
|
|
90
|
+
vi.unstubAllGlobals();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
async function mount(path: string, props: Record<string, unknown>) {
|
|
94
|
+
const { default: component } = await import(path);
|
|
95
|
+
app = createApp({ render: () => h(component, props) });
|
|
96
|
+
app.component("UButton", ButtonStub);
|
|
97
|
+
app.component("UTooltip", SlotStub);
|
|
98
|
+
app.mount(host);
|
|
99
|
+
await nextTick();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const mountBar = (actions: unknown[]) =>
|
|
103
|
+
mount(
|
|
104
|
+
"../layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue",
|
|
105
|
+
{ actions },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const buttons = () => [...host.querySelectorAll("button")];
|
|
109
|
+
|
|
110
|
+
describe("resolveLinkVariables", () => {
|
|
111
|
+
const context = {
|
|
112
|
+
routeParams: { id: "ws 1" },
|
|
113
|
+
routeQuery: { workspace: "web", tab: "a&b" },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it("fills the page's params and query, encoded", () => {
|
|
117
|
+
expect(
|
|
118
|
+
resolveLinkVariables(
|
|
119
|
+
"/ws/{{params.id}}?workspace={{query.workspace}}&tab={{query.tab}}",
|
|
120
|
+
context,
|
|
121
|
+
),
|
|
122
|
+
).toBe("/ws/ws%201?workspace=web&tab=a%26b");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("leaves out a query pair the page has no value for", () => {
|
|
126
|
+
expect(
|
|
127
|
+
resolveLinkVariables(
|
|
128
|
+
"/t?missing={{query.locale}}&workspace={{query.workspace}}#keys",
|
|
129
|
+
context,
|
|
130
|
+
),
|
|
131
|
+
).toBe("/t?workspace=web#keys");
|
|
132
|
+
expect(resolveLinkVariables("/t?locale={{query.locale}}", context)).toBe(
|
|
133
|
+
"/t",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("fills a query key carrying dots", () => {
|
|
138
|
+
expect(
|
|
139
|
+
resolveLinkVariables("/logs?period={{query.api.period}}", {
|
|
140
|
+
routeQuery: { "api.period": "7d" },
|
|
141
|
+
}),
|
|
142
|
+
).toBe("/logs?period=7d");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("empties any other token left without a value", () => {
|
|
146
|
+
expect(resolveLinkVariables("/t/{{params.slug}}/edit", context)).toBe(
|
|
147
|
+
"/t//edit",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("page query helpers", () => {
|
|
153
|
+
it("reads one value per name, the last, as the server does", () => {
|
|
154
|
+
expect(
|
|
155
|
+
pageQueryValues({ workspace: "web", tag: ["a", "b"], empty: null }),
|
|
156
|
+
).toEqual({ workspace: "web", tag: "b" });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("compares queries in any order", () => {
|
|
160
|
+
expect(isSamePageQuery({ a: "1", b: "2" }, { b: "2", a: "1" })).toBe(true);
|
|
161
|
+
expect(isSamePageQuery({ a: "1" }, { a: "1", b: "2" })).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("leaves out the keys that only record the view's state", () => {
|
|
165
|
+
const base = { workspace: "shop" };
|
|
166
|
+
expect(
|
|
167
|
+
isSamePageQuery(base, {
|
|
168
|
+
workspace: "shop",
|
|
169
|
+
record: "42",
|
|
170
|
+
live: "0",
|
|
171
|
+
"api.period": "24h",
|
|
172
|
+
"api.compare": "previous",
|
|
173
|
+
"api.from": "2026-10-01",
|
|
174
|
+
}),
|
|
175
|
+
).toBe(true);
|
|
176
|
+
expect(isSamePageQuery(base, { workspace: "blog", record: "42" })).toBe(
|
|
177
|
+
false,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("asks the layout URL for a query", () => {
|
|
182
|
+
expect(layoutUrlWithQuery("/p/pagelayout", { workspace: "a b" })).toBe(
|
|
183
|
+
"/p/pagelayout?workspace=a+b",
|
|
184
|
+
);
|
|
185
|
+
expect(layoutUrlWithQuery("/p/pagelayout", {})).toBe("/p/pagelayout");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("a header button's target URL", () => {
|
|
190
|
+
it("links to the page's scope, and follows it", async () => {
|
|
191
|
+
await mountBar([
|
|
192
|
+
{
|
|
193
|
+
id: "matrix",
|
|
194
|
+
label: "Open matrix",
|
|
195
|
+
target: {
|
|
196
|
+
type: "page",
|
|
197
|
+
url: "/modules/lang/translations?workspace={{query.workspace}}&missingIn={{params.locale}}",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
expect(buttons()[0]?.dataset.to).toBe(
|
|
202
|
+
"/modules/lang/translations?workspace=web&missingIn=de",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
route.query = { workspace: "docs" };
|
|
206
|
+
await nextTick();
|
|
207
|
+
expect(buttons()[0]?.dataset.to).toBe(
|
|
208
|
+
"/modules/lang/translations?workspace=docs&missingIn=de",
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
route.query = {};
|
|
212
|
+
await nextTick();
|
|
213
|
+
expect(buttons()[0]?.dataset.to).toBe(
|
|
214
|
+
"/modules/lang/translations?missingIn=de",
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("runs an API target on the page's record", async () => {
|
|
219
|
+
await mountBar([
|
|
220
|
+
{
|
|
221
|
+
id: "export",
|
|
222
|
+
label: "Export",
|
|
223
|
+
target: {
|
|
224
|
+
type: "api",
|
|
225
|
+
url: "/api/lang/export?workspace={{query.workspace}}",
|
|
226
|
+
successMessage: "Done",
|
|
227
|
+
},
|
|
228
|
+
confirm: { title: "Export?" },
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
buttons()[0]?.click();
|
|
232
|
+
expect(handleCustomButton).toHaveBeenCalledWith(
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
target: expect.objectContaining({
|
|
235
|
+
url: "/api/lang/export?workspace=web",
|
|
236
|
+
}),
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("a header button's availability", () => {
|
|
243
|
+
const download = (disabled: boolean) => ({
|
|
244
|
+
id: "download",
|
|
245
|
+
label: "Download",
|
|
246
|
+
target: { type: "page", url: "/download" },
|
|
247
|
+
disabled,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const mountOutlet = (servedQuery?: Record<string, string>) =>
|
|
251
|
+
mount(
|
|
252
|
+
"../layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue",
|
|
253
|
+
{ actions: [download(false)], servedQuery },
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
it("is not asked again while the query is the one it was served for", async () => {
|
|
257
|
+
await mountOutlet({ workspace: "web" });
|
|
258
|
+
expect(authFetch).not.toHaveBeenCalled();
|
|
259
|
+
expect(buttons()[0]?.disabled).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("is asked again for the page's query when it changes", async () => {
|
|
263
|
+
authFetch.mockResolvedValue({
|
|
264
|
+
components: {},
|
|
265
|
+
layout: { options: { headerActions: [download(true)] } },
|
|
266
|
+
});
|
|
267
|
+
await mountOutlet({ workspace: "web" });
|
|
268
|
+
|
|
269
|
+
route.query = { workspace: "gone" };
|
|
270
|
+
await nextTick();
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
272
|
+
expect(authFetch).toHaveBeenCalledWith(`${LAYOUT_URL}?workspace=gone`);
|
|
273
|
+
expect(buttons()[0]?.disabled).toBe(true);
|
|
274
|
+
|
|
275
|
+
route.query = { workspace: "web" };
|
|
276
|
+
await nextTick();
|
|
277
|
+
expect(buttons()[0]?.disabled).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("is never asked again when no button declares one", async () => {
|
|
281
|
+
await mountOutlet(undefined);
|
|
282
|
+
route.query = { workspace: "gone" };
|
|
283
|
+
await nextTick();
|
|
284
|
+
expect(authFetch).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* text field, and nothing at all on a page without `live`.
|
|
6
6
|
*/
|
|
7
7
|
import { createApp, h, nextTick, type App } from "vue";
|
|
8
|
-
import {
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { usePageLive } from "../layers/dms-ui/app/composables/live/usePageLive";
|
|
9
12
|
import { defineShortcuts } from "@nuxt/ui/runtime/composables/defineShortcuts.js";
|
|
10
13
|
import {
|
|
11
14
|
createPageLiveController,
|
|
@@ -33,6 +36,9 @@ async function createToggle(
|
|
|
33
36
|
h(LiveToggleBlock, { componentId: "live", pageId: "api.logs", ...props }),
|
|
34
37
|
});
|
|
35
38
|
toggle.component("UIcon", () => h("i"));
|
|
39
|
+
toggle.component("UButton", (buttonProps: { label?: string }, { attrs }) =>
|
|
40
|
+
h("button", { ...attrs, type: "button" }, buttonProps.label),
|
|
41
|
+
);
|
|
36
42
|
toggle.mount(target);
|
|
37
43
|
await nextTick();
|
|
38
44
|
return toggle;
|
|
@@ -160,3 +166,83 @@ it("switches once per L with two toggles on the page", async () => {
|
|
|
160
166
|
expect(controller.isPaused.value).toBe(false);
|
|
161
167
|
second.unmount();
|
|
162
168
|
});
|
|
169
|
+
|
|
170
|
+
describe("the pill variant and the paused state", () => {
|
|
171
|
+
const pill = (value: string) =>
|
|
172
|
+
host.querySelector<HTMLButtonElement>(`button[data-value="${value}"]`);
|
|
173
|
+
const resumeButton = () =>
|
|
174
|
+
host.querySelector<HTMLButtonElement>('[data-action="resume"]');
|
|
175
|
+
|
|
176
|
+
it("draws a Live pill with a pulsing dot beside Pause", async () => {
|
|
177
|
+
installLive();
|
|
178
|
+
await mountToggle({ variant: "pill" });
|
|
179
|
+
expect(pill("direct")?.getAttribute("aria-pressed")).toBe("true");
|
|
180
|
+
expect(pill("direct")?.textContent).toContain("dms.live.direct");
|
|
181
|
+
expect(host.querySelector(".dms-live-toggle__dot")?.className).toContain(
|
|
182
|
+
"animate-pulse",
|
|
183
|
+
);
|
|
184
|
+
expect(host.textContent).toContain("dms.live.updated_seconds:0");
|
|
185
|
+
expect(resumeButton()).toBeNull();
|
|
186
|
+
|
|
187
|
+
pill("pause")?.click();
|
|
188
|
+
await nextTick();
|
|
189
|
+
expect(live?.isPaused.value).toBe(true);
|
|
190
|
+
expect(resumeButton()).not.toBeNull();
|
|
191
|
+
|
|
192
|
+
resumeButton()?.click();
|
|
193
|
+
await nextTick();
|
|
194
|
+
expect(live?.isPaused.value).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("says what was missed, with the module's label and breakdown", async () => {
|
|
198
|
+
const controller = installLive();
|
|
199
|
+
await mountToggle({
|
|
200
|
+
variant: "pill",
|
|
201
|
+
missedLabel: "$api.live.new_requests",
|
|
202
|
+
});
|
|
203
|
+
controller.pause();
|
|
204
|
+
const block = controller.subscribe(() => {});
|
|
205
|
+
block.trigger({ topic: "api", payload: { count: 40 } });
|
|
206
|
+
usePageLive().setMissedDetail({
|
|
207
|
+
text: "$api.live.in_5xx",
|
|
208
|
+
params: { count: 2 },
|
|
209
|
+
tone: "error",
|
|
210
|
+
});
|
|
211
|
+
await nextTick();
|
|
212
|
+
|
|
213
|
+
const status = host.querySelector(".dms-live-toggle__status");
|
|
214
|
+
expect(status?.textContent).toContain(
|
|
215
|
+
"dms.live.paused · api.live.new_requests:40",
|
|
216
|
+
);
|
|
217
|
+
expect(status?.querySelector(".text-error")?.textContent).toContain(
|
|
218
|
+
"api.live.in_5xx:2",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("names the reason a component holds the page for", async () => {
|
|
223
|
+
installLive();
|
|
224
|
+
await mountToggle();
|
|
225
|
+
usePageLive().hold("$api.live.typing_in_tester");
|
|
226
|
+
await nextTick();
|
|
227
|
+
expect(host.textContent).toContain(
|
|
228
|
+
"dms.live.paused · api.live.typing_in_tester",
|
|
229
|
+
);
|
|
230
|
+
expect(host.textContent).not.toContain("dms.live.updated");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("reads Live in en-GB, Direct in fr-FR", () => {
|
|
234
|
+
const locale = (name: string) =>
|
|
235
|
+
JSON.parse(
|
|
236
|
+
readFileSync(
|
|
237
|
+
resolve(__dirname, `../layers/dms-ui/i18n/locales/ui-${name}.json`),
|
|
238
|
+
"utf8",
|
|
239
|
+
),
|
|
240
|
+
).dms.live;
|
|
241
|
+
expect(locale("en-GB").direct).toBe("Live");
|
|
242
|
+
expect(locale("fr-FR").direct).toBe("Direct");
|
|
243
|
+
for (const key of ["paused", "resume", "reason_hidden", "polling"]) {
|
|
244
|
+
expect(locale("en-GB")[key]).toBeTruthy();
|
|
245
|
+
expect(locale("fr-FR")[key]).toBeTruthy();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -38,6 +38,7 @@ it("loads a page registered after setup without changing the route", async () =>
|
|
|
38
38
|
await nextTick();
|
|
39
39
|
expect(loadPageLayout).toHaveBeenCalledExactlyOnceWith(
|
|
40
40
|
"/new-page/pagelayout",
|
|
41
|
+
{},
|
|
41
42
|
);
|
|
42
43
|
expect(pageLayout.value).toBe("/new-page/pagelayout");
|
|
43
44
|
expect(route.path).toBe("/new-page");
|