@antelopejs/dms 0.7.1 → 0.7.2
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 +35 -0
- package/dist/hooks/tenant-instances.d.ts +1 -0
- package/dist/hooks/tenant-instances.js +13 -0
- package/dist/hooks/tenant-instances.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +31 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
- package/dist/test/unit/utils/tenant-instances.test.js +53 -0
- package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/tenant-instances.d.ts +2 -0
- package/dist/utils/tenant-instances.js +38 -0
- package/dist/utils/tenant-instances.js.map +1 -0
- package/dist/validation/auth.schema.d.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
- package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
- package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
- package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +5 -5
- package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
- package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
- package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
- package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
- package/frontend-vue/tests/block-route-params.test.ts +48 -0
- package/frontend-vue/tests/code-block.test.ts +155 -0
- package/frontend-vue/tests/code-highlight.test.ts +72 -0
- package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
- package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
- package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
- package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
- package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
- package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
- package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
- package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
- package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
- package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
- package/frontend-vue/tests/page-fill-height.test.ts +20 -3
- package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
- package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
- package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
- package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
- package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
- package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
- package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
- package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
- package/frontend-vue/tests/table-view-sources.test.ts +67 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +14 -3
- package/skills/dms-pages/REFERENCE.md +17 -1
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
18
|
import {
|
|
19
19
|
replaceUrlVariables,
|
|
20
|
+
resolveTextVariables,
|
|
20
21
|
resolveUrlVariables,
|
|
21
22
|
} from "../layers/dms-ui/app/build/utils/urlVariables";
|
|
22
23
|
|
|
@@ -96,6 +97,53 @@ describe("resolveUrlVariables", () => {
|
|
|
96
97
|
);
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
it("encodes the values it fills, so each stays one parameter", () => {
|
|
101
|
+
expect(
|
|
102
|
+
resolveUrlVariables("/api/log/{{params.id}}?route={{query.route}}", {
|
|
103
|
+
routeParams: { id: "a b" },
|
|
104
|
+
routeQuery: { route: "GET /api/a?b=c&d" },
|
|
105
|
+
}),
|
|
106
|
+
).toBe("/api/log/a%20b?route=GET%20%2Fapi%2Fa%3Fb%3Dc%26d");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// The router keeps the page path percent-encoded, and route parameters are
|
|
110
|
+
// read from it.
|
|
111
|
+
it("does not encode a route parameter twice", () => {
|
|
112
|
+
expect(
|
|
113
|
+
replaceUrlVariables("/api/ws/{{params.id}}", {
|
|
114
|
+
routeParams: { id: "caf%C3%A9%20bar" },
|
|
115
|
+
routeQuery: {},
|
|
116
|
+
}),
|
|
117
|
+
).toBe("/api/ws/caf%C3%A9%20bar");
|
|
118
|
+
expect(
|
|
119
|
+
replaceUrlVariables("/api/ws/{{params.id}}", {
|
|
120
|
+
routeParams: { id: "100%" },
|
|
121
|
+
routeQuery: {},
|
|
122
|
+
}),
|
|
123
|
+
).toBe("/api/ws/100%25");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("writes response values as answered", () => {
|
|
127
|
+
expect(
|
|
128
|
+
replaceUrlVariables("{{response.redirectPath}}&tab={{query.tab}}", {
|
|
129
|
+
routeQuery: { tab: "a&b" },
|
|
130
|
+
response: { redirectPath: "/settings/users?invited=1" },
|
|
131
|
+
}),
|
|
132
|
+
).toBe("/settings/users?invited=1&tab=a%26b");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("fills a plain value without encoding it", () => {
|
|
136
|
+
expect(
|
|
137
|
+
resolveTextVariables("{{query.status}} / {{params.id}}", {
|
|
138
|
+
routeParams: { id: "ws-1" },
|
|
139
|
+
routeQuery: { status: "on hold" },
|
|
140
|
+
}),
|
|
141
|
+
).toBe("on hold / ws-1");
|
|
142
|
+
expect(resolveTextVariables("{{query.status}}", { routeQuery: {} })).toBe(
|
|
143
|
+
undefined,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
99
147
|
it("leaves a URL without tokens as it is", () => {
|
|
100
148
|
expect(resolveUrlVariables("/api/plain", { routeQuery: {} })).toBe(
|
|
101
149
|
"/api/plain",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { type App, createApp, h, ref } from "vue";
|
|
4
|
+
|
|
5
|
+
const data = ref<Record<string, unknown> | null>(null);
|
|
6
|
+
const error = ref<unknown>(null);
|
|
7
|
+
const isLoading = ref(false);
|
|
8
|
+
const fetchOptions = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
11
|
+
useChartFetch: (options: unknown) => {
|
|
12
|
+
fetchOptions(options);
|
|
13
|
+
return { data, isLoading, error, refresh: async () => {} };
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../layers/dms-core/app/composables/watch/useWatch", () => ({
|
|
18
|
+
useWatch: () => ({ state: ref({}) }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let app: App | undefined;
|
|
22
|
+
|
|
23
|
+
const flushPromises = () => new Promise((resolve) => setTimeout(resolve));
|
|
24
|
+
let host: HTMLDivElement;
|
|
25
|
+
|
|
26
|
+
async function mount(path: string, props: Record<string, unknown>) {
|
|
27
|
+
const { default: component } = await import(path);
|
|
28
|
+
app = createApp({ setup: () => () => h(component, props) });
|
|
29
|
+
for (const name of ["UIcon", "USkeleton", "UButton"]) {
|
|
30
|
+
app.component(name, (_, { attrs }) =>
|
|
31
|
+
h("i", { ...attrs, "data-stub": name }),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
app.mount(host);
|
|
35
|
+
await flushPromises();
|
|
36
|
+
await vi.dynamicImportSettled();
|
|
37
|
+
await flushPromises();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mountSnippet = (props: Record<string, unknown>) =>
|
|
41
|
+
mount("../layers/dms-ui/app/components/code-snippet/CodeSnippet.vue", props);
|
|
42
|
+
const mountBlock = (props: Record<string, unknown>) =>
|
|
43
|
+
mount("../layers/dms-ui/app/components/blocks/CodeBlock.vue", {
|
|
44
|
+
componentId: "code",
|
|
45
|
+
pageId: "page",
|
|
46
|
+
...props,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const pre = () => host.querySelector("pre")!;
|
|
50
|
+
const copyButton = () => host.querySelector("button");
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
data.value = null;
|
|
54
|
+
error.value = null;
|
|
55
|
+
isLoading.value = false;
|
|
56
|
+
fetchOptions.mockClear();
|
|
57
|
+
vi.stubGlobal("useI18n", () => ({ locale: ref("en-GB"), t: String }));
|
|
58
|
+
vi.stubGlobal("useTranslation", () => ({ processI18n: String }));
|
|
59
|
+
host = document.createElement("div");
|
|
60
|
+
document.body.append(host);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
app?.unmount();
|
|
65
|
+
host.remove();
|
|
66
|
+
vi.unstubAllGlobals();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("DmsCodeSnippet", () => {
|
|
70
|
+
it("highlights the code in its language, under a head naming it", async () => {
|
|
71
|
+
await mountSnippet({
|
|
72
|
+
code: '{ "id": 1 }',
|
|
73
|
+
language: "json",
|
|
74
|
+
title: "Body",
|
|
75
|
+
});
|
|
76
|
+
expect(pre().textContent).toBe('{ "id": 1 }');
|
|
77
|
+
expect(pre().querySelector('[style*="--ui-info"]')?.textContent).toBe(
|
|
78
|
+
'"id"',
|
|
79
|
+
);
|
|
80
|
+
expect(host.textContent).toContain("Body");
|
|
81
|
+
expect(host.textContent).toContain("JSON");
|
|
82
|
+
expect(copyButton()).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("reads an alias as its language", async () => {
|
|
86
|
+
await mountSnippet({ code: "pnpm install", language: "bash" });
|
|
87
|
+
expect(pre().dataset.language).toBe("shell");
|
|
88
|
+
expect(host.textContent).toContain("Shell");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("drops the copy button and the head when nothing is left to show", async () => {
|
|
92
|
+
await mountSnippet({ code: "plain words", copy: false });
|
|
93
|
+
expect(copyButton()).toBeNull();
|
|
94
|
+
expect(pre().previousElementSibling).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("wraps long lines or scrolls them, and caps the height in lines", async () => {
|
|
98
|
+
await mountSnippet({ code: "a\nb\nc", wrap: true, maxLines: 2 });
|
|
99
|
+
expect(pre().className).toContain("whitespace-pre-wrap");
|
|
100
|
+
expect(pre().style.maxHeight).toBe("54px");
|
|
101
|
+
app?.unmount();
|
|
102
|
+
await mountSnippet({ code: "a" });
|
|
103
|
+
expect(pre().className).toContain("whitespace-pre");
|
|
104
|
+
expect(pre().className).not.toContain("whitespace-pre-wrap");
|
|
105
|
+
expect(pre().style.maxHeight).toBe("");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("CodeBlock", () => {
|
|
110
|
+
it("shows its static code and title", async () => {
|
|
111
|
+
await mountBlock({ code: "SELECT 1", language: "sql", title: "Query" });
|
|
112
|
+
expect(pre().textContent).toBe("SELECT 1");
|
|
113
|
+
expect(host.textContent).toContain("Query");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("replaces the code and the language with what its route answers", async () => {
|
|
117
|
+
data.value = { code: "echo hi", language: "shell" };
|
|
118
|
+
await mountBlock({
|
|
119
|
+
code: "old",
|
|
120
|
+
language: "json",
|
|
121
|
+
fetchUrl: "/api/snippet",
|
|
122
|
+
});
|
|
123
|
+
expect(fetchOptions).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({ fetchUrl: "/api/snippet" }),
|
|
125
|
+
);
|
|
126
|
+
expect(pre().textContent).toBe("echo hi");
|
|
127
|
+
expect(pre().dataset.language).toBe("shell");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("draws placeholder lines until the first answer", async () => {
|
|
131
|
+
isLoading.value = true;
|
|
132
|
+
await mountBlock({ fetchUrl: "/api/snippet" });
|
|
133
|
+
expect(host.querySelector("pre")).toBeNull();
|
|
134
|
+
expect(host.querySelector("[aria-busy]")).not.toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("offers a retry when the first load fails", async () => {
|
|
138
|
+
error.value = new Error("network");
|
|
139
|
+
await mountBlock({ fetchUrl: "/api/snippet" });
|
|
140
|
+
expect(host.querySelector("pre")).toBeNull();
|
|
141
|
+
expect(host.textContent).toContain("dms.blocks.load_error");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("EmptyState block", () => {
|
|
146
|
+
it("shows a snippet between its text and its actions", async () => {
|
|
147
|
+
await mount("../layers/dms-ui/app/components/blocks/EmptyStateBlock.vue", {
|
|
148
|
+
title: "No email sent yet",
|
|
149
|
+
code: { content: "await mail.send({ to })", language: "typescript" },
|
|
150
|
+
});
|
|
151
|
+
expect(pre().textContent).toBe("await mail.send({ to })");
|
|
152
|
+
expect(host.textContent).toContain("TypeScript");
|
|
153
|
+
expect(copyButton()).not.toBeNull();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
type CodeToken,
|
|
4
|
+
highlightCode,
|
|
5
|
+
} from "../layers/dms-ui/app/build/utils/codeHighlight";
|
|
6
|
+
import { resolveCodeLanguage } from "../layers/dms-ui/app/build/utils/codeLanguages";
|
|
7
|
+
|
|
8
|
+
const KEYWORD = "var(--ui-primary)";
|
|
9
|
+
const STRING = "var(--ui-success)";
|
|
10
|
+
const NUMBER = "var(--ui-warning)";
|
|
11
|
+
const COMMENT = "var(--ui-text-dimmed)";
|
|
12
|
+
const PROPERTY = "var(--ui-info)";
|
|
13
|
+
|
|
14
|
+
const colorOf = (tokens: CodeToken[], text: string) =>
|
|
15
|
+
tokens.find((token) => token.text === text)?.style?.color;
|
|
16
|
+
|
|
17
|
+
const joined = (tokens: CodeToken[]) =>
|
|
18
|
+
tokens.map((token) => token.text).join("");
|
|
19
|
+
|
|
20
|
+
describe("read-only code highlighting", () => {
|
|
21
|
+
it("keeps every character of the code, in order", () => {
|
|
22
|
+
const code = '{\n "name": "media",\n "size": 42\n}';
|
|
23
|
+
expect(joined(highlightCode(code, "json"))).toBe(code);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("colours JSON keys, strings and numbers", () => {
|
|
27
|
+
const tokens = highlightCode('{ "name": "media", "size": 42 }', "json");
|
|
28
|
+
expect(colorOf(tokens, '"name"')).toBe(PROPERTY);
|
|
29
|
+
expect(colorOf(tokens, '"media"')).toBe(STRING);
|
|
30
|
+
expect(colorOf(tokens, "42")).toBe(NUMBER);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("reads TypeScript, SQL and HTML with the editor's languages", () => {
|
|
34
|
+
expect(
|
|
35
|
+
colorOf(highlightCode("const x: number = 1;", "typescript"), "const"),
|
|
36
|
+
).toBe(KEYWORD);
|
|
37
|
+
expect(
|
|
38
|
+
colorOf(highlightCode("SELECT id FROM users", "sql"), "SELECT"),
|
|
39
|
+
).toBe(KEYWORD);
|
|
40
|
+
expect(colorOf(highlightCode('<a href="/">x</a>', "html"), "a")).toBe(
|
|
41
|
+
KEYWORD,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("marks a shell command, its flags, strings, variables and comments", () => {
|
|
46
|
+
const code =
|
|
47
|
+
'curl -X POST "$API_URL" \\\n -H "Accept: json" # send\necho $HOME | grep root';
|
|
48
|
+
const tokens = highlightCode(code, "shell");
|
|
49
|
+
expect(joined(tokens)).toBe(code);
|
|
50
|
+
expect(colorOf(tokens, "curl")).toBe(KEYWORD);
|
|
51
|
+
expect(colorOf(tokens, "-X")).toBe(PROPERTY);
|
|
52
|
+
expect(colorOf(tokens, '"$API_URL"')).toBe(STRING);
|
|
53
|
+
expect(colorOf(tokens, "-H")).toBe(PROPERTY);
|
|
54
|
+
expect(colorOf(tokens, "# send")).toBe(COMMENT);
|
|
55
|
+
expect(colorOf(tokens, "echo")).toBe(KEYWORD);
|
|
56
|
+
expect(colorOf(tokens, "$HOME")).toBe(NUMBER);
|
|
57
|
+
expect(colorOf(tokens, "grep")).toBe(KEYWORD);
|
|
58
|
+
expect(colorOf(tokens, "root")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows plain text and unknown languages as written", () => {
|
|
62
|
+
expect(highlightCode("SELECT 1", "text")).toEqual([{ text: "SELECT 1" }]);
|
|
63
|
+
expect(highlightCode("x = 1", "cobol")).toEqual([{ text: "x = 1" }]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("reads the usual aliases as their language", () => {
|
|
67
|
+
expect(resolveCodeLanguage("bash")).toBe("shell");
|
|
68
|
+
expect(resolveCodeLanguage("TS")).toBe("typescript");
|
|
69
|
+
expect(resolveCodeLanguage("plain")).toBe("text");
|
|
70
|
+
expect(resolveCodeLanguage(undefined)).toBe("text");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -57,6 +57,11 @@ vi.mock(
|
|
|
57
57
|
() => ({ useComponentEvent: () => ({}) }),
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
+
vi.mock(
|
|
61
|
+
"../layers/dms-ui/app/build/composables/actions/useActionTargets",
|
|
62
|
+
() => ({ useActionTargets: () => ({ handleCustomButton: () => {} }) }),
|
|
63
|
+
);
|
|
64
|
+
|
|
60
65
|
// The generic components only lay out what the block resolved: their props
|
|
61
66
|
// are what is checked here.
|
|
62
67
|
const propsProbe = (name: string, keys: string[]) => ({
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* ActivityFeed, Meter, TopListCard and NavCardGrid draw a composed text
|
|
4
|
+
* wherever they draw a text — from their options or from the JSON their
|
|
5
|
+
* route answers — in the reader's language, and keep drawing plain and `$`
|
|
6
|
+
* strings as before.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
computed,
|
|
10
|
+
createApp,
|
|
11
|
+
defineComponent,
|
|
12
|
+
h,
|
|
13
|
+
reactive,
|
|
14
|
+
ref,
|
|
15
|
+
type Component,
|
|
16
|
+
} from "vue";
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
import { createI18n } from "vue-i18n";
|
|
19
|
+
import { resolveI18nKey } from "../layers/dms-core/app/composables/translation/useTranslation";
|
|
20
|
+
import { setRegionalPreferencesSource } from "../layers/dms-core/app/utils/regional";
|
|
21
|
+
|
|
22
|
+
const NOW = Date.UTC(2026, 9, 10, 12);
|
|
23
|
+
const THREE_MINUTES_AGO = new Date(NOW - 3 * 60 * 1000).toISOString();
|
|
24
|
+
|
|
25
|
+
const i18n = createI18n({
|
|
26
|
+
legacy: false,
|
|
27
|
+
locale: "en",
|
|
28
|
+
fallbackLocale: "en",
|
|
29
|
+
missingWarn: false,
|
|
30
|
+
fallbackWarn: false,
|
|
31
|
+
messages: {
|
|
32
|
+
en: {
|
|
33
|
+
mail: {
|
|
34
|
+
to_review:
|
|
35
|
+
"no message to review | {count} message to review | {count} messages to review",
|
|
36
|
+
bounced: "{count} bounced on {list}",
|
|
37
|
+
device: "{browser} on {os}",
|
|
38
|
+
signed_in: "Signed in on {device}",
|
|
39
|
+
sent: "Sent {when}",
|
|
40
|
+
},
|
|
41
|
+
media: {
|
|
42
|
+
files: "{count} file | {count} files",
|
|
43
|
+
used: "{size} used",
|
|
44
|
+
quota: "{used} of {total}",
|
|
45
|
+
library: "Library",
|
|
46
|
+
new: "New",
|
|
47
|
+
},
|
|
48
|
+
api: { last_call: "Last call {at}" },
|
|
49
|
+
},
|
|
50
|
+
fr: {
|
|
51
|
+
mail: {
|
|
52
|
+
to_review:
|
|
53
|
+
"aucun message à traiter | {count} message à traiter | {count} messages à traiter",
|
|
54
|
+
bounced: "{count} rejetés sur {list}",
|
|
55
|
+
device: "{browser} sous {os}",
|
|
56
|
+
signed_in: "Connexion sur {device}",
|
|
57
|
+
sent: "Envoyé {when}",
|
|
58
|
+
},
|
|
59
|
+
media: {
|
|
60
|
+
files: "{count} fichier | {count} fichiers",
|
|
61
|
+
used: "{size} utilisés",
|
|
62
|
+
quota: "{used} sur {total}",
|
|
63
|
+
library: "Médiathèque",
|
|
64
|
+
new: "Nouveau",
|
|
65
|
+
},
|
|
66
|
+
api: { last_call: "Dernier appel {at}" },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const meterData = ref<Record<string, unknown> | null>(null);
|
|
72
|
+
|
|
73
|
+
vi.stubGlobal("useAuthFetch", () => ({
|
|
74
|
+
$authFetch: async () => ({ items: [] }),
|
|
75
|
+
}));
|
|
76
|
+
vi.stubGlobal("useDmsRoute", () => reactive({ query: {} }));
|
|
77
|
+
|
|
78
|
+
vi.mock("../layers/dms-core/app/composables/watch/useWatch", () => ({
|
|
79
|
+
useWatch: () => ({ state: ref({}) }),
|
|
80
|
+
}));
|
|
81
|
+
vi.mock(
|
|
82
|
+
"../layers/dms-core/app/composables/components/useComponentEvent",
|
|
83
|
+
() => ({ useComponentEvent: () => ({}) }),
|
|
84
|
+
);
|
|
85
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
86
|
+
useChartFetch: () => ({
|
|
87
|
+
data: meterData,
|
|
88
|
+
isLoading: ref(false),
|
|
89
|
+
error: ref(null),
|
|
90
|
+
refresh: async () => {},
|
|
91
|
+
}),
|
|
92
|
+
}));
|
|
93
|
+
vi.mock(
|
|
94
|
+
"#dms-layout/app/build/composables/navigation/useCategoryNavCards",
|
|
95
|
+
() => ({
|
|
96
|
+
useCategoryNavCards: () => ({ cards: computed(() => []) }),
|
|
97
|
+
usePreviewEntryVeil: () => ({
|
|
98
|
+
state: () => null,
|
|
99
|
+
label: () => "",
|
|
100
|
+
detail: () => undefined,
|
|
101
|
+
isActive: ref(false),
|
|
102
|
+
}),
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// The generic components only lay out what the block resolved: their props
|
|
107
|
+
// are what is checked here.
|
|
108
|
+
const { propsProbe } = vi.hoisted(() => ({
|
|
109
|
+
propsProbe: (name: string, keys: string[]) => async () => {
|
|
110
|
+
const vue = await import("vue");
|
|
111
|
+
return {
|
|
112
|
+
default: vue.defineComponent({
|
|
113
|
+
props: keys,
|
|
114
|
+
setup:
|
|
115
|
+
(props, { slots }) =>
|
|
116
|
+
() =>
|
|
117
|
+
vue.h("div", [
|
|
118
|
+
vue.h("pre", { "data-probe": name }, JSON.stringify(props)),
|
|
119
|
+
slots.default?.(),
|
|
120
|
+
]),
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
vi.mock(
|
|
126
|
+
"../layers/dms-ui/app/components/activity/ActivityItem.vue",
|
|
127
|
+
propsProbe("activity-item", ["title", "subtitle", "trailing"]),
|
|
128
|
+
);
|
|
129
|
+
vi.mock(
|
|
130
|
+
"../layers/dms-ui/app/components/meter/Meter.vue",
|
|
131
|
+
propsProbe("meter", ["label", "hint", "valueLabel", "segments"]),
|
|
132
|
+
);
|
|
133
|
+
vi.mock(
|
|
134
|
+
"../layers/dms-ui/app/components/card/NavCard.vue",
|
|
135
|
+
propsProbe("nav-card", ["title", "description", "state", "tag", "readout"]),
|
|
136
|
+
);
|
|
137
|
+
vi.mock(
|
|
138
|
+
"../layers/dms-ui/app/build/components/permission/PermissionVeil.vue",
|
|
139
|
+
propsProbe("veil", []),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const ActivityFeed = (
|
|
143
|
+
await import("../layers/dms-ui/app/components/activity-feed/ActivityFeed.vue")
|
|
144
|
+
).default;
|
|
145
|
+
const MeterBlock = (
|
|
146
|
+
await import("../layers/dms-ui/app/components/meter/MeterBlock.vue")
|
|
147
|
+
).default;
|
|
148
|
+
const TopListRow = (
|
|
149
|
+
await import(
|
|
150
|
+
"../layers/dms-ui/app/components/top-list/internal/TopListRow.vue"
|
|
151
|
+
)
|
|
152
|
+
).default;
|
|
153
|
+
const NavCardGridBlock = (
|
|
154
|
+
await import("../layers/dms-ui/app/components/blocks/NavCardGridBlock.vue")
|
|
155
|
+
).default;
|
|
156
|
+
|
|
157
|
+
const mounted: Array<{ unmount: () => void }> = [];
|
|
158
|
+
const plainSpaces = (text: string) => text.replace(/[\u00a0\u202f]/g, " ");
|
|
159
|
+
|
|
160
|
+
function mount(component: Component, props: Record<string, unknown>) {
|
|
161
|
+
const root = document.createElement("div");
|
|
162
|
+
const app = createApp(component, props);
|
|
163
|
+
app.component(
|
|
164
|
+
"DmsCard",
|
|
165
|
+
defineComponent({
|
|
166
|
+
setup:
|
|
167
|
+
(_, { slots }) =>
|
|
168
|
+
() =>
|
|
169
|
+
h("div", slots.default?.()),
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
app.mount(root);
|
|
173
|
+
mounted.push(app);
|
|
174
|
+
return root;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function probes(root: HTMLElement, name: string): Record<string, unknown>[] {
|
|
178
|
+
return [...root.querySelectorAll(`[data-probe="${name}"]`)].map((probe) =>
|
|
179
|
+
JSON.parse(plainSpaces(probe.textContent ?? "{}")),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const count = (value: number) => ({ type: "count", value });
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
vi.useFakeTimers({ now: NOW, toFake: ["Date"] });
|
|
187
|
+
i18n.global.locale.value = "en";
|
|
188
|
+
meterData.value = null;
|
|
189
|
+
setRegionalPreferencesSource(() => ({ timeZone: "UTC" }));
|
|
190
|
+
vi.stubGlobal("useI18n", () => ({
|
|
191
|
+
t: i18n.global.t,
|
|
192
|
+
locale: i18n.global.locale,
|
|
193
|
+
}));
|
|
194
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
195
|
+
processI18n: (key: string, params?: Record<string, unknown> | null) =>
|
|
196
|
+
resolveI18nKey(i18n.global.t, key, params),
|
|
197
|
+
}));
|
|
198
|
+
vi.stubGlobal("useDmsCookie", () => ref<string[]>([]));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
mounted.splice(0).forEach((app) => app.unmount());
|
|
203
|
+
setRegionalPreferencesSource(() => ({}));
|
|
204
|
+
vi.useRealTimers();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("ActivityFeed", () => {
|
|
208
|
+
const items = [
|
|
209
|
+
{
|
|
210
|
+
id: "review",
|
|
211
|
+
title: { key: "mail.to_review", params: { count: count(3) } },
|
|
212
|
+
meta: [
|
|
213
|
+
{
|
|
214
|
+
key: "mail.sent",
|
|
215
|
+
params: { when: { type: "relative", value: THREE_MINUTES_AGO } },
|
|
216
|
+
},
|
|
217
|
+
"Inbox",
|
|
218
|
+
],
|
|
219
|
+
time: { key: "media.new" },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "bounced",
|
|
223
|
+
title: "$mail.bounced",
|
|
224
|
+
params: { count: count(1284), list: "Newsletter" },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "legacy",
|
|
228
|
+
title: "$mail.signed_in",
|
|
229
|
+
meta: ["$mail.device"],
|
|
230
|
+
params: { device: "$mail.device", browser: "Chrome", os: "Windows" },
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
it("composes titles, details and the trailing text of its entries", () => {
|
|
235
|
+
const root = mount(ActivityFeed, { items, groupByDay: false, card: false });
|
|
236
|
+
|
|
237
|
+
expect(probes(root, "activity-item")).toEqual([
|
|
238
|
+
{
|
|
239
|
+
title: "3 messages to review",
|
|
240
|
+
subtitle: "Sent 3 minutes ago · Inbox",
|
|
241
|
+
trailing: "New",
|
|
242
|
+
},
|
|
243
|
+
{ title: "1,284 bounced on Newsletter" },
|
|
244
|
+
{
|
|
245
|
+
title: "Signed in on Chrome on Windows",
|
|
246
|
+
subtitle: "Chrome on Windows",
|
|
247
|
+
},
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("picks the plural form of a `$` title from its count value, in French", () => {
|
|
252
|
+
i18n.global.locale.value = "fr";
|
|
253
|
+
const root = mount(ActivityFeed, {
|
|
254
|
+
items: [
|
|
255
|
+
{ title: "$mail.to_review", params: { count: count(1) } },
|
|
256
|
+
{
|
|
257
|
+
title: "$mail.signed_in",
|
|
258
|
+
params: { device: "$mail.device", browser: "Firefox", os: "Linux" },
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
groupByDay: false,
|
|
262
|
+
card: false,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(probes(root, "activity-item").map((item) => item.title)).toEqual([
|
|
266
|
+
"1 message à traiter",
|
|
267
|
+
"Connexion sur Firefox sous Linux",
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("MeterBlock", () => {
|
|
273
|
+
it("composes the hint, the value text and the segment legends", () => {
|
|
274
|
+
i18n.global.locale.value = "fr";
|
|
275
|
+
const root = mount(MeterBlock, {
|
|
276
|
+
label: "$media.library",
|
|
277
|
+
hint: { key: "media.files", params: { count: count(2) } },
|
|
278
|
+
valueLabel: {
|
|
279
|
+
key: "media.quota",
|
|
280
|
+
params: {
|
|
281
|
+
used: { type: "number", value: 0.42, format: "percent" },
|
|
282
|
+
total: "10 Go",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
segments: [
|
|
286
|
+
{
|
|
287
|
+
value: 3,
|
|
288
|
+
label: { key: "media.files", params: { count: count(1) } },
|
|
289
|
+
},
|
|
290
|
+
{ value: 2, label: "Plain" },
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(probes(root, "meter")).toEqual([
|
|
295
|
+
{
|
|
296
|
+
label: "Médiathèque",
|
|
297
|
+
hint: "2 fichiers",
|
|
298
|
+
valueLabel: "42 % sur 10 Go",
|
|
299
|
+
segments: [
|
|
300
|
+
{ value: 3, label: "1 fichier" },
|
|
301
|
+
{ value: 2, label: "Plain" },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("composes the texts its route answers", () => {
|
|
308
|
+
meterData.value = {
|
|
309
|
+
value: 4,
|
|
310
|
+
max: 10,
|
|
311
|
+
hint: { key: "media.used", params: { size: "4 GB" } },
|
|
312
|
+
};
|
|
313
|
+
const root = mount(MeterBlock, { fetchUrl: "/api/storage" });
|
|
314
|
+
|
|
315
|
+
expect(probes(root, "meter")[0]?.hint).toBe("4 GB used");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("TopListRow", () => {
|
|
320
|
+
it("composes the title and the description of a row", () => {
|
|
321
|
+
i18n.global.locale.value = "fr";
|
|
322
|
+
const root = mount(TopListRow, {
|
|
323
|
+
item: {
|
|
324
|
+
id: "orders",
|
|
325
|
+
title: "$media.library",
|
|
326
|
+
description: {
|
|
327
|
+
key: "api.last_call",
|
|
328
|
+
params: { at: { type: "relative", value: THREE_MINUTES_AGO } },
|
|
329
|
+
},
|
|
330
|
+
value: 12,
|
|
331
|
+
},
|
|
332
|
+
showRank: false,
|
|
333
|
+
rankLabel: "1",
|
|
334
|
+
rankClass: "",
|
|
335
|
+
hasAnyIcon: false,
|
|
336
|
+
showSparkline: false,
|
|
337
|
+
sparklineAccent: "primary",
|
|
338
|
+
showDelta: false,
|
|
339
|
+
formattedValue: "12",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(plainSpaces(root.textContent ?? "")).toContain("Médiathèque");
|
|
343
|
+
expect(plainSpaces(root.textContent ?? "")).toContain(
|
|
344
|
+
"Dernier appel il y a 3 minutes",
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("keeps a plain title as written", () => {
|
|
349
|
+
const root = mount(TopListRow, {
|
|
350
|
+
item: { id: 1, title: "GET /orders", value: 1 },
|
|
351
|
+
showRank: false,
|
|
352
|
+
rankLabel: "1",
|
|
353
|
+
rankClass: "",
|
|
354
|
+
hasAnyIcon: false,
|
|
355
|
+
showSparkline: false,
|
|
356
|
+
sparklineAccent: "primary",
|
|
357
|
+
showDelta: false,
|
|
358
|
+
formattedValue: "1",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(root.textContent).toContain("GET /orders");
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("NavCardGridBlock", () => {
|
|
366
|
+
it("composes the title, description, tag, state and readout of a card", () => {
|
|
367
|
+
const root = mount(NavCardGridBlock, {
|
|
368
|
+
items: [
|
|
369
|
+
{
|
|
370
|
+
icon: "i-ph-images",
|
|
371
|
+
to: "/media",
|
|
372
|
+
title: { key: "media.library" },
|
|
373
|
+
description: "$media.new",
|
|
374
|
+
tag: { key: "media.new" },
|
|
375
|
+
state: { key: "media.files", params: { count: count(1284) } },
|
|
376
|
+
readout: [{ key: "media.used", params: { size: "4 GB" } }, "plain"],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(probes(root, "nav-card")).toEqual([
|
|
382
|
+
{
|
|
383
|
+
title: "Library",
|
|
384
|
+
description: "New",
|
|
385
|
+
tag: "New",
|
|
386
|
+
state: "1,284 files",
|
|
387
|
+
readout: ["4 GB used", "plain"],
|
|
388
|
+
},
|
|
389
|
+
]);
|
|
390
|
+
});
|
|
391
|
+
});
|