@agent-native/toolkit 0.213.1 → 0.216.0
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/agent-native.eject.json +9 -0
- package/dist/app/agentkit/react/chat.d.ts.map +1 -1
- package/dist/app/agentkit/react/chat.js +2 -2
- package/dist/app/agentkit/react/chat.js.map +1 -1
- package/dist/app/agentkit/react/components.d.ts +4 -1
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +73 -13
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +19 -1
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +7 -1
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/agentkit/react/index.d.ts +2 -0
- package/dist/app/agentkit/react/index.d.ts.map +1 -1
- package/dist/app/agentkit/react/index.js +2 -0
- package/dist/app/agentkit/react/index.js.map +1 -1
- package/dist/app/agentkit/react/message-content-order.d.ts +21 -0
- package/dist/app/agentkit/react/message-content-order.d.ts.map +1 -0
- package/dist/app/agentkit/react/message-content-order.js +173 -0
- package/dist/app/agentkit/react/message-content-order.js.map +1 -0
- package/dist/app/agentkit/react/root.d.ts +6 -1
- package/dist/app/agentkit/react/root.d.ts.map +1 -1
- package/dist/app/agentkit/react/root.js +2 -2
- package/dist/app/agentkit/react/root.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +45 -8
- package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
- package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
- package/dist/app/agentkit/react/transcript-layout.js +34 -0
- package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +9 -4
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentPanel.d.ts +16 -2
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js +26 -5
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/chat/index.d.ts +2 -0
- package/dist/app/chat/chat/index.d.ts.map +1 -1
- package/dist/app/chat/chat/index.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +15 -0
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/chat/chat/surface-types.js.map +1 -1
- package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
- package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
- package/dist/app/chat/chat/tool-call-display.js +10 -9
- package/dist/app/chat/chat/tool-call-display.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartRenderer.js +300 -33
- package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.d.ts +3 -2
- package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.js +18 -9
- package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataInsightsWidget.js +1 -1
- package/dist/app/chat/chat/widgets/DataInsightsWidget.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataTableWidget.d.ts +3 -2
- package/dist/app/chat/chat/widgets/DataTableWidget.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataTableWidget.js +10 -66
- package/dist/app/chat/chat/widgets/DataTableWidget.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +11 -0
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -0
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +133 -0
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -0
- package/dist/app/chat/chat/widgets/builtin-tool-renderers.js +2 -2
- package/dist/app/chat/chat/widgets/builtin-tool-renderers.js.map +1 -1
- package/dist/app/chat/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/app/chat/mcp-apps/McpAppRenderer.js +6 -5
- package/dist/app/chat/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts +2 -0
- package/dist/app/chat/mcp-apps/mcp-app-theme.d.ts.map +1 -0
- package/dist/app/chat/mcp-apps/mcp-app-theme.js +11 -0
- package/dist/app/chat/mcp-apps/mcp-app-theme.js.map +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +9 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +9 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +9 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +9 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +9 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +9 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +9 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +9 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +9 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +9 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +9 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +9 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/providers/AppProviders.d.ts +2 -1
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +4 -3
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/providers/ThemeFamilySync.d.ts +2 -0
- package/dist/app/providers/ThemeFamilySync.d.ts.map +1 -0
- package/dist/app/providers/ThemeFamilySync.js +6 -0
- package/dist/app/providers/ThemeFamilySync.js.map +1 -0
- package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.js +2 -1
- package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
- package/dist/app/settings/account/ThemeFamilySettings.d.ts +2 -0
- package/dist/app/settings/account/ThemeFamilySettings.d.ts.map +1 -0
- package/dist/app/settings/account/ThemeFamilySettings.js +31 -0
- package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -0
- package/dist/app/settings/account/search-entries.d.ts.map +1 -1
- package/dist/app/settings/account/search-entries.js +6 -0
- package/dist/app/settings/account/search-entries.js.map +1 -1
- package/dist/app/shared/DefaultSpinner.d.ts.map +1 -1
- package/dist/app/shared/DefaultSpinner.js +4 -5
- package/dist/app/shared/DefaultSpinner.js.map +1 -1
- package/dist/app/shared/ErrorBoundary.d.ts +1 -0
- package/dist/app/shared/ErrorBoundary.d.ts.map +1 -1
- package/dist/app/shared/ErrorBoundary.js +27 -12
- package/dist/app/shared/ErrorBoundary.js.map +1 -1
- package/dist/app/shared/PoweredByBadge.d.ts.map +1 -1
- package/dist/app/shared/PoweredByBadge.js +8 -49
- package/dist/app/shared/PoweredByBadge.js.map +1 -1
- package/dist/app/styles/agent-conversation.css +0 -23
- package/dist/app/styles/agent-native.css +1 -1
- package/dist/app/styles/theme-families.css +66 -0
- package/dist/composer/ComposerContextBanner.d.ts +24 -0
- package/dist/composer/ComposerContextBanner.d.ts.map +1 -0
- package/dist/composer/ComposerContextBanner.js +9 -0
- package/dist/composer/ComposerContextBanner.js.map +1 -0
- package/dist/composer/ComposerModePicker.d.ts +26 -0
- package/dist/composer/ComposerModePicker.d.ts.map +1 -0
- package/dist/composer/ComposerModePicker.js +14 -0
- package/dist/composer/ComposerModePicker.js.map +1 -0
- package/dist/composer/index.d.ts +2 -0
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +2 -0
- package/dist/composer/index.js.map +1 -1
- package/dist/styles.css +1 -0
- package/package.json +3 -3
- package/src/app/agentkit/react/chat.tsx +2 -0
- package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
- package/src/app/agentkit/react/components.spec.tsx +40 -1
- package/src/app/agentkit/react/components.tsx +186 -31
- package/src/app/agentkit/react/context.tsx +29 -0
- package/src/app/agentkit/react/index.ts +2 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +289 -4
- package/src/app/agentkit/react/message-content-order.spec.ts +190 -0
- package/src/app/agentkit/react/message-content-order.ts +246 -0
- package/src/app/agentkit/react/root.tsx +11 -0
- package/src/app/agentkit/react/styles.css +45 -8
- package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
- package/src/app/agentkit/react/transcript-layout.ts +97 -0
- package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +58 -0
- package/src/app/chat/AgentKitAssistantChat.tsx +15 -1
- package/src/app/chat/AgentPanel.header.spec.ts +72 -14
- package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
- package/src/app/chat/AgentPanel.tsx +111 -55
- package/src/app/chat/chat/index.ts +2 -0
- package/src/app/chat/chat/surface-types.ts +18 -0
- package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
- package/src/app/chat/chat/tool-call-display.tsx +40 -8
- package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +849 -0
- package/src/app/chat/chat/widgets/DataChartRenderer.tsx +567 -72
- package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +432 -0
- package/src/app/chat/chat/widgets/DataChartWidget.ssr.spec.tsx +25 -0
- package/src/app/chat/chat/widgets/DataChartWidget.tsx +58 -33
- package/src/app/chat/chat/widgets/DataInsightsWidget.tsx +2 -2
- package/src/app/chat/chat/widgets/DataTableWidget.spec.tsx +37 -3
- package/src/app/chat/chat/widgets/DataTableWidget.tsx +22 -86
- package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +321 -0
- package/src/app/chat/chat/widgets/builtin-tool-renderers.tsx +5 -5
- package/src/app/chat/mcp-apps/McpAppRenderer.tsx +10 -5
- package/src/app/chat/mcp-apps/mcp-app-theme.spec.ts +30 -0
- package/src/app/chat/mcp-apps/mcp-app-theme.ts +11 -0
- package/src/app/i18n/catalogs/ar-SA.ts +10 -0
- package/src/app/i18n/catalogs/de-DE.ts +11 -0
- package/src/app/i18n/catalogs/en-US.ts +11 -0
- package/src/app/i18n/catalogs/es-ES.ts +12 -0
- package/src/app/i18n/catalogs/fr-FR.ts +12 -0
- package/src/app/i18n/catalogs/hi-IN.ts +10 -0
- package/src/app/i18n/catalogs/ja-JP.ts +11 -0
- package/src/app/i18n/catalogs/ko-KR.ts +10 -0
- package/src/app/i18n/catalogs/pt-BR.ts +11 -0
- package/src/app/i18n/catalogs/zh-CN.ts +9 -0
- package/src/app/i18n/catalogs/zh-TW.ts +10 -0
- package/src/app/providers/AppProviders.spec.tsx +71 -0
- package/src/app/providers/AppProviders.tsx +10 -0
- package/src/app/providers/ThemeFamilySync.tsx +6 -0
- package/src/app/settings/account/PreferencesSettings.spec.tsx +13 -2
- package/src/app/settings/account/PreferencesSettings.tsx +8 -0
- package/src/app/settings/account/ThemeFamilySettings.tsx +61 -0
- package/src/app/settings/account/search-entries.ts +6 -0
- package/src/app/shared/DefaultSpinner.spec.tsx +14 -0
- package/src/app/shared/DefaultSpinner.tsx +4 -5
- package/src/app/shared/ErrorBoundary.spec.tsx +45 -2
- package/src/app/shared/ErrorBoundary.tsx +37 -12
- package/src/app/shared/PoweredByBadge.spec.tsx +16 -0
- package/src/app/shared/PoweredByBadge.tsx +8 -50
- package/src/app/styles/agent-conversation.css +0 -23
- package/src/app/styles/agent-native.css +1 -1
- package/src/app/styles/agent-native.spec.ts +18 -0
- package/src/app/styles/theme-families.css +66 -0
- package/src/app/styles/theme-families.spec.ts +29 -0
- package/src/composer/ComposerContextBanner.spec.tsx +100 -0
- package/src/composer/ComposerContextBanner.tsx +77 -0
- package/src/composer/ComposerModePicker.spec.tsx +101 -0
- package/src/composer/ComposerModePicker.tsx +98 -0
- package/src/composer/index.ts +9 -0
- package/src/styles.css +1 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
|
|
4
|
+
import React, { act } from "react";
|
|
5
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { createToolkitI18nCatalog } from "../../../i18n.js";
|
|
9
|
+
import type {
|
|
10
|
+
DataWidgetAction,
|
|
11
|
+
DataWidgetChartAction,
|
|
12
|
+
DataWidgetDisplayAction,
|
|
13
|
+
} from "./data-widget-types.js";
|
|
14
|
+
import { DataChartWidget } from "./DataChartWidget.js";
|
|
15
|
+
|
|
16
|
+
const mocks = vi.hoisted(() => ({
|
|
17
|
+
writeClipboardText: vi.fn(),
|
|
18
|
+
toast: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@agent-native/toolkit/clipboard", () => ({
|
|
22
|
+
writeClipboardText: mocks.writeClipboardText,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../../../../hooks/use-toast.js", () => ({ toast: mocks.toast }));
|
|
26
|
+
|
|
27
|
+
vi.mock("./DataChartRenderer.js", () => ({
|
|
28
|
+
DataChartRenderer: ({
|
|
29
|
+
chart,
|
|
30
|
+
}: {
|
|
31
|
+
chart: {
|
|
32
|
+
contentHeight?: number;
|
|
33
|
+
contentPadding?: number;
|
|
34
|
+
labelFontFamily?: string;
|
|
35
|
+
labelFontSize?: number;
|
|
36
|
+
};
|
|
37
|
+
}) => (
|
|
38
|
+
<div
|
|
39
|
+
data-testid="chart-surface"
|
|
40
|
+
data-content-height={chart.contentHeight}
|
|
41
|
+
data-content-padding={chart.contentPadding}
|
|
42
|
+
data-label-font-family={chart.labelFontFamily}
|
|
43
|
+
data-label-font-size={chart.labelFontSize}
|
|
44
|
+
/>
|
|
45
|
+
),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const toolkitI18nCatalog = createToolkitI18nCatalog({ messages: {} });
|
|
49
|
+
const initialBasePath = process.env.VITE_APP_BASE_PATH;
|
|
50
|
+
const roots: Root[] = [];
|
|
51
|
+
|
|
52
|
+
async function renderWidget(
|
|
53
|
+
chartActions?: DataWidgetChartAction[],
|
|
54
|
+
displayActions: DataWidgetDisplayAction[] = [
|
|
55
|
+
{
|
|
56
|
+
id: "details",
|
|
57
|
+
type: "link",
|
|
58
|
+
label: "Open details",
|
|
59
|
+
href: "/reports/1/details",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "query",
|
|
63
|
+
type: "copy",
|
|
64
|
+
label: "Copy query",
|
|
65
|
+
text: "select count(*)",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "unsafe",
|
|
69
|
+
type: "link",
|
|
70
|
+
label: "Unsafe",
|
|
71
|
+
href: "javascript:alert(1)",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
sampled = false,
|
|
75
|
+
chartDataCount = 1,
|
|
76
|
+
truncated = false,
|
|
77
|
+
) {
|
|
78
|
+
const container = document.createElement("div");
|
|
79
|
+
document.body.appendChild(container);
|
|
80
|
+
const root = createRoot(container);
|
|
81
|
+
roots.push(root);
|
|
82
|
+
|
|
83
|
+
await act(async () => {
|
|
84
|
+
root.render(
|
|
85
|
+
<AgentNativeI18nProvider
|
|
86
|
+
catalog={toolkitI18nCatalog}
|
|
87
|
+
persistPreference={false}
|
|
88
|
+
>
|
|
89
|
+
<DataChartWidget
|
|
90
|
+
chart={{
|
|
91
|
+
type: "bar",
|
|
92
|
+
title: "Responses by day",
|
|
93
|
+
xKey: "day",
|
|
94
|
+
series: [{ key: "count", label: "Responses" }],
|
|
95
|
+
data: Array.from({ length: chartDataCount }, (_, index) => ({
|
|
96
|
+
day: `Day ${index}`,
|
|
97
|
+
count: index,
|
|
98
|
+
})),
|
|
99
|
+
sampled,
|
|
100
|
+
truncated,
|
|
101
|
+
actions: chartActions,
|
|
102
|
+
}}
|
|
103
|
+
display={{
|
|
104
|
+
primaryAction: {
|
|
105
|
+
label: "Open report",
|
|
106
|
+
href: "/reports/1?tab=chart",
|
|
107
|
+
},
|
|
108
|
+
sources: [
|
|
109
|
+
{
|
|
110
|
+
id: "warehouse",
|
|
111
|
+
label: "Warehouse",
|
|
112
|
+
href: "/sources/warehouse",
|
|
113
|
+
},
|
|
114
|
+
{ id: "local", label: "Local data" },
|
|
115
|
+
],
|
|
116
|
+
actions: displayActions,
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
</AgentNativeI18nProvider>,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return container;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function openActionsMenu(container: HTMLElement) {
|
|
127
|
+
const trigger = container.querySelector<HTMLButtonElement>(
|
|
128
|
+
'button[aria-label="Chart actions"]',
|
|
129
|
+
);
|
|
130
|
+
expect(trigger).not.toBeNull();
|
|
131
|
+
|
|
132
|
+
await act(async () => {
|
|
133
|
+
trigger?.dispatchEvent(
|
|
134
|
+
new PointerEvent("pointerdown", {
|
|
135
|
+
bubbles: true,
|
|
136
|
+
button: 0,
|
|
137
|
+
pointerType: "mouse",
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
await Promise.resolve();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return document.querySelector<HTMLElement>("[role=menu]");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("DataChartWidget display contract", () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
149
|
+
process.env.VITE_APP_BASE_PATH = "/mounted";
|
|
150
|
+
window.history.replaceState({}, "", "/chat");
|
|
151
|
+
mocks.writeClipboardText.mockResolvedValue(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
for (const root of roots.splice(0)) {
|
|
156
|
+
act(() => root.unmount());
|
|
157
|
+
}
|
|
158
|
+
document.body.innerHTML = "";
|
|
159
|
+
window.history.replaceState({}, "", "/");
|
|
160
|
+
process.env.VITE_APP_BASE_PATH = initialBasePath;
|
|
161
|
+
mocks.writeClipboardText.mockReset();
|
|
162
|
+
mocks.toast.mockReset();
|
|
163
|
+
vi.unstubAllGlobals();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("renders source chips and navigates safe chart links", async () => {
|
|
167
|
+
const container = await renderWidget();
|
|
168
|
+
const primary = Array.from(container.querySelectorAll("a")).find((link) =>
|
|
169
|
+
link.textContent?.includes("Open report"),
|
|
170
|
+
);
|
|
171
|
+
const source = Array.from(container.querySelectorAll("a")).find((link) =>
|
|
172
|
+
link.textContent?.includes("Warehouse"),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(primary?.getAttribute("href")).toBe("/mounted/reports/1?tab=chart");
|
|
176
|
+
expect(source?.getAttribute("href")).toBe("/mounted/sources/warehouse");
|
|
177
|
+
expect(container.textContent).toContain("Local data");
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
primary?.dispatchEvent(
|
|
181
|
+
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(window.location.pathname).toBe("/mounted/reports/1");
|
|
186
|
+
expect(window.location.search).toBe("?tab=chart");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("announces incomplete chart data and stays quiet for complete data", async () => {
|
|
190
|
+
const cappedContainer = await renderWidget(
|
|
191
|
+
undefined,
|
|
192
|
+
undefined,
|
|
193
|
+
false,
|
|
194
|
+
201,
|
|
195
|
+
);
|
|
196
|
+
const cappedDisclosure = cappedContainer.querySelector('[role="status"]');
|
|
197
|
+
|
|
198
|
+
expect(cappedDisclosure?.textContent).toBe(
|
|
199
|
+
"This chart shows only some of the available rows.",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const sampledContainer = await renderWidget(undefined, undefined, true);
|
|
203
|
+
expect(sampledContainer.querySelector('[role="status"]')?.textContent).toBe(
|
|
204
|
+
"This chart shows only some of the available rows.",
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const sampledAndCappedContainer = await renderWidget(
|
|
208
|
+
undefined,
|
|
209
|
+
undefined,
|
|
210
|
+
true,
|
|
211
|
+
201,
|
|
212
|
+
);
|
|
213
|
+
expect(
|
|
214
|
+
sampledAndCappedContainer.querySelector('[role="status"]')?.textContent,
|
|
215
|
+
).toBe("This chart shows only some of the available rows.");
|
|
216
|
+
|
|
217
|
+
const clampedContainer = await renderWidget(
|
|
218
|
+
undefined,
|
|
219
|
+
undefined,
|
|
220
|
+
true,
|
|
221
|
+
200,
|
|
222
|
+
true,
|
|
223
|
+
);
|
|
224
|
+
expect(clampedContainer.querySelector('[role="status"]')?.textContent).toBe(
|
|
225
|
+
"This chart shows only some of the available rows.",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const completeContainer = await renderWidget();
|
|
229
|
+
expect(completeContainer.querySelector('[role="status"]')).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("opens overflow links and copies the requested text", async () => {
|
|
233
|
+
const container = await renderWidget();
|
|
234
|
+
const menu = await openActionsMenu(container);
|
|
235
|
+
expect(menu).not.toBeNull();
|
|
236
|
+
expect(menu?.textContent).toContain("Open details");
|
|
237
|
+
expect(menu?.textContent).toContain("Copy query");
|
|
238
|
+
expect(menu?.textContent).not.toContain("Unsafe");
|
|
239
|
+
|
|
240
|
+
const details = Array.from(menu?.querySelectorAll("a") ?? []).find((link) =>
|
|
241
|
+
link.textContent?.includes("Open details"),
|
|
242
|
+
);
|
|
243
|
+
expect(details?.getAttribute("href")).toBe("/mounted/reports/1/details");
|
|
244
|
+
|
|
245
|
+
await act(async () => {
|
|
246
|
+
details?.dispatchEvent(
|
|
247
|
+
new MouseEvent("click", { bubbles: true, cancelable: true, button: 0 }),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
expect(window.location.pathname).toBe("/mounted/reports/1/details");
|
|
251
|
+
|
|
252
|
+
const secondContainer = await renderWidget();
|
|
253
|
+
const secondMenu = await openActionsMenu(secondContainer);
|
|
254
|
+
const copy = Array.from(
|
|
255
|
+
secondMenu?.querySelectorAll<HTMLElement>("[role=menuitem]") ?? [],
|
|
256
|
+
).find((item) => item.textContent?.includes("Copy query"));
|
|
257
|
+
|
|
258
|
+
await act(async () => {
|
|
259
|
+
copy?.click();
|
|
260
|
+
await Promise.resolve();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(mocks.writeClipboardText).toHaveBeenCalledWith("select count(*)");
|
|
264
|
+
expect(mocks.toast).toHaveBeenCalledWith({ title: "Copied" });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("renders actions attached to the chart payload", async () => {
|
|
268
|
+
const container = await renderWidget([
|
|
269
|
+
{
|
|
270
|
+
label: "Explore results",
|
|
271
|
+
href: "/reports/1/explore",
|
|
272
|
+
icon: "explore",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
label: "Download image",
|
|
276
|
+
href: "/reports/1/image",
|
|
277
|
+
icon: "camera",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
label: "Save to notebook",
|
|
281
|
+
href: "/notebooks/new",
|
|
282
|
+
icon: "notebook",
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
285
|
+
const menu = await openActionsMenu(container);
|
|
286
|
+
|
|
287
|
+
expect(menu?.textContent).toContain("Explore results");
|
|
288
|
+
expect(menu?.textContent).toContain("Download image");
|
|
289
|
+
expect(menu?.textContent).toContain("Save to notebook");
|
|
290
|
+
expect(
|
|
291
|
+
menu?.querySelector('a[href="/mounted/reports/1/explore"] svg'),
|
|
292
|
+
).not.toBeNull();
|
|
293
|
+
expect(
|
|
294
|
+
menu?.querySelector('a[href="/mounted/reports/1/image"] svg'),
|
|
295
|
+
).not.toBeNull();
|
|
296
|
+
expect(
|
|
297
|
+
menu?.querySelector('a[href="/mounted/notebooks/new"] svg'),
|
|
298
|
+
).not.toBeNull();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("renders an icon action directly with an accessible label", async () => {
|
|
302
|
+
const container = await renderWidget([
|
|
303
|
+
{
|
|
304
|
+
label: "Download image",
|
|
305
|
+
href: "/reports/1/image",
|
|
306
|
+
icon: "camera",
|
|
307
|
+
presentation: "icon",
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
const directAction = container.querySelector<HTMLAnchorElement>(
|
|
311
|
+
'a[aria-label="Download image"]',
|
|
312
|
+
);
|
|
313
|
+
const menu = await openActionsMenu(container);
|
|
314
|
+
|
|
315
|
+
expect(directAction?.getAttribute("href")).toBe("/mounted/reports/1/image");
|
|
316
|
+
expect(directAction?.getAttribute("title")).toBe("Download image");
|
|
317
|
+
expect(directAction?.querySelector("svg")).not.toBeNull();
|
|
318
|
+
expect(menu?.textContent).not.toContain("Download image");
|
|
319
|
+
expect(menu?.textContent).toContain("Open details");
|
|
320
|
+
expect(menu?.textContent).toContain("Copy query");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("hides the overflow control when all actions are direct icons", async () => {
|
|
324
|
+
const container = await renderWidget(
|
|
325
|
+
[],
|
|
326
|
+
[
|
|
327
|
+
{
|
|
328
|
+
id: "direct-download",
|
|
329
|
+
type: "link",
|
|
330
|
+
label: "Download image",
|
|
331
|
+
href: "/reports/1/image",
|
|
332
|
+
icon: "camera",
|
|
333
|
+
presentation: "icon",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(
|
|
339
|
+
container.querySelector('a[aria-label="Download image"]'),
|
|
340
|
+
).not.toBeNull();
|
|
341
|
+
expect(
|
|
342
|
+
container.querySelector('button[aria-label="Chart actions"]'),
|
|
343
|
+
).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("keeps chart actions as links when payload metadata includes a type", async () => {
|
|
347
|
+
const container = await renderWidget([
|
|
348
|
+
{
|
|
349
|
+
type: "copy",
|
|
350
|
+
label: "Explore results",
|
|
351
|
+
href: "/reports/1/explore",
|
|
352
|
+
} as DataWidgetAction,
|
|
353
|
+
]);
|
|
354
|
+
const menu = await openActionsMenu(container);
|
|
355
|
+
|
|
356
|
+
expect(
|
|
357
|
+
menu?.querySelector('a[href="/mounted/reports/1/explore"]'),
|
|
358
|
+
).not.toBeNull();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("does not render executable primary-action URLs", async () => {
|
|
362
|
+
const container = document.createElement("div");
|
|
363
|
+
document.body.appendChild(container);
|
|
364
|
+
const root = createRoot(container);
|
|
365
|
+
roots.push(root);
|
|
366
|
+
|
|
367
|
+
await act(async () => {
|
|
368
|
+
root.render(
|
|
369
|
+
<AgentNativeI18nProvider
|
|
370
|
+
catalog={toolkitI18nCatalog}
|
|
371
|
+
persistPreference={false}
|
|
372
|
+
>
|
|
373
|
+
<DataChartWidget
|
|
374
|
+
chart={{
|
|
375
|
+
type: "bar",
|
|
376
|
+
xKey: "day",
|
|
377
|
+
series: [{ key: "count", label: "Count" }],
|
|
378
|
+
data: [],
|
|
379
|
+
}}
|
|
380
|
+
display={{
|
|
381
|
+
primaryAction: { label: "Unsafe", href: "javascript:alert(1)" },
|
|
382
|
+
}}
|
|
383
|
+
/>
|
|
384
|
+
</AgentNativeI18nProvider>,
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(container.querySelector("a")).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("uses the chart-selected content height without adding body padding", async () => {
|
|
392
|
+
const container = document.createElement("div");
|
|
393
|
+
document.body.appendChild(container);
|
|
394
|
+
const root = createRoot(container);
|
|
395
|
+
roots.push(root);
|
|
396
|
+
|
|
397
|
+
await act(async () => {
|
|
398
|
+
root.render(
|
|
399
|
+
<AgentNativeI18nProvider
|
|
400
|
+
catalog={toolkitI18nCatalog}
|
|
401
|
+
persistPreference={false}
|
|
402
|
+
>
|
|
403
|
+
<DataChartWidget
|
|
404
|
+
chart={{
|
|
405
|
+
type: "bar",
|
|
406
|
+
xKey: "day",
|
|
407
|
+
contentHeight: 400,
|
|
408
|
+
contentPadding: 16,
|
|
409
|
+
labelFontFamily: "sans",
|
|
410
|
+
labelFontSize: 14,
|
|
411
|
+
series: [{ key: "count", label: "Count" }],
|
|
412
|
+
data: [],
|
|
413
|
+
}}
|
|
414
|
+
/>
|
|
415
|
+
</AgentNativeI18nProvider>,
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const frame = container.querySelector(".my-4");
|
|
420
|
+
const header = frame?.querySelector(".h-12");
|
|
421
|
+
const chartSurface = frame?.querySelector<HTMLElement>(
|
|
422
|
+
"[data-testid=chart-surface]",
|
|
423
|
+
);
|
|
424
|
+
expect(frame?.className).toContain("rounded-[3px]");
|
|
425
|
+
expect(header?.className).toContain("border-b");
|
|
426
|
+
expect(chartSurface?.dataset.contentHeight).toBe("400");
|
|
427
|
+
expect(chartSurface?.dataset.contentPadding).toBe("16");
|
|
428
|
+
expect(chartSurface?.dataset.labelFontFamily).toBe("sans");
|
|
429
|
+
expect(chartSurface?.dataset.labelFontSize).toBe("14");
|
|
430
|
+
expect(frame?.querySelector(".p-3")).toBeNull();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
@@ -18,11 +18,36 @@ describe("DataChartWidget SSR", () => {
|
|
|
18
18
|
series: [{ key: "count", label: "Count" }],
|
|
19
19
|
data: [{ day: "2026-06-17", count: 3 }],
|
|
20
20
|
}}
|
|
21
|
+
display={{
|
|
22
|
+
primaryAction: {
|
|
23
|
+
label: "Open report",
|
|
24
|
+
href: "https://reports.example.com/one",
|
|
25
|
+
},
|
|
26
|
+
}}
|
|
21
27
|
/>,
|
|
22
28
|
);
|
|
23
29
|
|
|
24
30
|
expect(html).toContain("Submissions");
|
|
25
31
|
expect(html).toContain("Chart");
|
|
26
32
|
expect(html).not.toContain("Loading chart");
|
|
33
|
+
expect(html).toContain('href="https://reports.example.com/one"');
|
|
34
|
+
|
|
35
|
+
const mailtoHtml = renderToString(
|
|
36
|
+
<DataChartWidget
|
|
37
|
+
chart={{
|
|
38
|
+
type: "bar",
|
|
39
|
+
xKey: "day",
|
|
40
|
+
series: [{ key: "count", label: "Count" }],
|
|
41
|
+
data: [],
|
|
42
|
+
}}
|
|
43
|
+
display={{
|
|
44
|
+
primaryAction: {
|
|
45
|
+
label: "Email report",
|
|
46
|
+
href: "mailto:reports@example.com",
|
|
47
|
+
},
|
|
48
|
+
}}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
expect(mailtoHtml).toContain('href="mailto:reports@example.com"');
|
|
27
52
|
});
|
|
28
53
|
});
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
useOptionalLocale,
|
|
4
|
-
useT,
|
|
5
|
-
} from "@agent-native/core/client/i18n";
|
|
6
|
-
import { IconChartBar } from "@tabler/icons-react";
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { DATA_WIDGET_MAX_CHART_POINTS } from "@agent-native/core/data-widgets";
|
|
7
3
|
import { lazy, Suspense, useEffect, useState } from "react";
|
|
8
4
|
|
|
9
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
DataChartWidget as DataChartWidgetData,
|
|
7
|
+
DataWidgetDisplay,
|
|
8
|
+
} from "./data-widget-types.js";
|
|
9
|
+
import {
|
|
10
|
+
DataWidgetDisplayControls,
|
|
11
|
+
DataWidgetSourceChips,
|
|
12
|
+
} from "./DataWidgetDisplayControls.js";
|
|
10
13
|
|
|
11
14
|
const LazyDataChartRenderer = lazy(() =>
|
|
12
15
|
import("./DataChartRenderer.js").then((module) => ({
|
|
@@ -14,47 +17,69 @@ const LazyDataChartRenderer = lazy(() =>
|
|
|
14
17
|
})),
|
|
15
18
|
);
|
|
16
19
|
|
|
17
|
-
export function DataChartWidget({
|
|
20
|
+
export function DataChartWidget({
|
|
21
|
+
chart,
|
|
22
|
+
display,
|
|
23
|
+
}: {
|
|
24
|
+
chart: DataChartWidgetData;
|
|
25
|
+
display?: DataWidgetDisplay;
|
|
26
|
+
}) {
|
|
18
27
|
const t = useT();
|
|
19
|
-
const locale = useOptionalLocale()?.locale ?? DEFAULT_LOCALE;
|
|
20
28
|
const [mounted, setMounted] = useState(false);
|
|
29
|
+
const contentHeight = chart.contentHeight;
|
|
30
|
+
const contentPadding = chart.contentPadding ?? 0;
|
|
31
|
+
const hasOmittedRows =
|
|
32
|
+
chart.sampled === true ||
|
|
33
|
+
chart.truncated === true ||
|
|
34
|
+
chart.data.length > DATA_WIDGET_MAX_CHART_POINTS;
|
|
21
35
|
|
|
22
36
|
useEffect(() => {
|
|
23
37
|
setMounted(true);
|
|
24
38
|
}, []);
|
|
25
39
|
|
|
26
40
|
const fallback = (
|
|
27
|
-
<div
|
|
41
|
+
<div
|
|
42
|
+
className={`box-border flex w-full items-center justify-center rounded-md bg-muted/30 text-xs text-muted-foreground ${contentHeight ? "" : "aspect-[16/9]"}`}
|
|
43
|
+
style={
|
|
44
|
+
contentHeight !== undefined || contentPadding > 0
|
|
45
|
+
? {
|
|
46
|
+
...(contentHeight !== undefined
|
|
47
|
+
? { height: `${contentHeight}px` }
|
|
48
|
+
: {}),
|
|
49
|
+
padding: `${contentPadding}px`,
|
|
50
|
+
}
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
>
|
|
28
54
|
{t("agentChat.widget.chart")}
|
|
29
55
|
</div>
|
|
30
56
|
);
|
|
31
57
|
|
|
32
58
|
return (
|
|
33
|
-
<div className="my-
|
|
34
|
-
<div className="flex items-center gap-2 border-b border-border px-3
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
<div className="truncate text-sm font-medium">
|
|
38
|
-
{chart.title ?? t("agentChat.widget.dataChart")}
|
|
39
|
-
</div>
|
|
40
|
-
<div className="text-[11px] text-muted-foreground">
|
|
41
|
-
{t("agentChat.widget.points", {
|
|
42
|
-
count: chart.data.length,
|
|
43
|
-
formattedCount: chart.data.length.toLocaleString(locale),
|
|
44
|
-
})}
|
|
45
|
-
{chart.sampled ? ` ${t("agentChat.widget.sampled")}` : ""}
|
|
46
|
-
</div>
|
|
59
|
+
<div className="my-4 overflow-hidden rounded-[3px] border border-border bg-background text-foreground shadow-sm">
|
|
60
|
+
<div className="flex h-12 items-center gap-2 border-b border-border px-3">
|
|
61
|
+
<div className="min-w-0 flex-1 truncate text-sm font-medium">
|
|
62
|
+
{chart.title ?? t("agentChat.widget.dataChart")}
|
|
47
63
|
</div>
|
|
64
|
+
<DataWidgetDisplayControls
|
|
65
|
+
display={display}
|
|
66
|
+
chartActions={chart.actions}
|
|
67
|
+
variant="secondary"
|
|
68
|
+
/>
|
|
48
69
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
{!mounted ? (
|
|
71
|
+
fallback
|
|
72
|
+
) : (
|
|
73
|
+
<Suspense fallback={fallback}>
|
|
74
|
+
<LazyDataChartRenderer chart={chart} />
|
|
75
|
+
</Suspense>
|
|
76
|
+
)}
|
|
77
|
+
{hasOmittedRows ? (
|
|
78
|
+
<p className="px-3 pb-3 text-xs text-muted-foreground" role="status">
|
|
79
|
+
{t("agentChat.widget.chartRowsOmitted")}
|
|
80
|
+
</p>
|
|
81
|
+
) : null}
|
|
82
|
+
<DataWidgetSourceChips sources={display?.sources ?? []} />
|
|
58
83
|
</div>
|
|
59
84
|
);
|
|
60
85
|
}
|
|
@@ -49,12 +49,12 @@ export function DataInsightsWidget({ result }: { result: DataWidgetResult }) {
|
|
|
49
49
|
</div>
|
|
50
50
|
</div>
|
|
51
51
|
{result.chartSeries ? (
|
|
52
|
-
<DataChartWidget chart={result.chartSeries} />
|
|
52
|
+
<DataChartWidget chart={result.chartSeries} display={result.display} />
|
|
53
53
|
) : null}
|
|
54
54
|
{result.table ? (
|
|
55
55
|
<DataTableWidget
|
|
56
56
|
table={result.table}
|
|
57
|
-
|
|
57
|
+
display={result.chartSeries ? undefined : result.display}
|
|
58
58
|
/>
|
|
59
59
|
) : null}
|
|
60
60
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
|
|
4
4
|
import React, { act } from "react";
|
|
5
5
|
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
8
|
import { downloadFile } from "../../../db-admin/export-utils.js";
|
|
9
9
|
import { createToolkitI18nCatalog } from "../../../i18n.js";
|
|
@@ -23,7 +23,16 @@ vi.mock("../../../db-admin/export-utils.js", async (importOriginal) => {
|
|
|
23
23
|
const roots: Root[] = [];
|
|
24
24
|
const initialBasePath = process.env.VITE_APP_BASE_PATH;
|
|
25
25
|
|
|
26
|
-
async function renderWidget(
|
|
26
|
+
async function renderWidget(
|
|
27
|
+
action: { label: string; href: string },
|
|
28
|
+
display?: {
|
|
29
|
+
sources?: Array<{ id?: string; label: string; href?: string }>;
|
|
30
|
+
actions?: Array<
|
|
31
|
+
| { id?: string; type: "link"; label: string; href: string }
|
|
32
|
+
| { id?: string; type: "copy"; label: string; text: string }
|
|
33
|
+
>;
|
|
34
|
+
},
|
|
35
|
+
) {
|
|
27
36
|
const container = document.createElement("div");
|
|
28
37
|
document.body.appendChild(container);
|
|
29
38
|
const root = createRoot(container);
|
|
@@ -36,6 +45,7 @@ async function renderWidget(action: { label: string; href: string }) {
|
|
|
36
45
|
>
|
|
37
46
|
<DataTableWidget
|
|
38
47
|
action={action}
|
|
48
|
+
display={display}
|
|
39
49
|
table={{
|
|
40
50
|
title: "Responses",
|
|
41
51
|
columns: [{ key: "name", label: "Name" }],
|
|
@@ -49,13 +59,18 @@ async function renderWidget(action: { label: string; href: string }) {
|
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
describe("DataTableWidget", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
64
|
+
});
|
|
65
|
+
|
|
52
66
|
afterEach(() => {
|
|
53
67
|
for (const root of roots.splice(0)) {
|
|
54
|
-
root.unmount();
|
|
68
|
+
act(() => root.unmount());
|
|
55
69
|
}
|
|
56
70
|
document.body.innerHTML = "";
|
|
57
71
|
process.env.VITE_APP_BASE_PATH = initialBasePath;
|
|
58
72
|
vi.mocked(downloadFile).mockClear();
|
|
73
|
+
vi.unstubAllGlobals();
|
|
59
74
|
});
|
|
60
75
|
|
|
61
76
|
it("does not render executable action URLs", async () => {
|
|
@@ -99,6 +114,25 @@ describe("DataTableWidget", () => {
|
|
|
99
114
|
).toBeNull();
|
|
100
115
|
});
|
|
101
116
|
|
|
117
|
+
it("renders table source chips and overflow actions from display metadata", async () => {
|
|
118
|
+
const container = await renderWidget(
|
|
119
|
+
{ label: "Open responses", href: "/forms/form-1" },
|
|
120
|
+
{
|
|
121
|
+
sources: [{ label: "Responses", href: "/sources/responses" }],
|
|
122
|
+
actions: [{ type: "copy", label: "Copy query", text: "select 1" }],
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
container.querySelector('button[aria-label="Table actions"]'),
|
|
128
|
+
).not.toBeNull();
|
|
129
|
+
expect(
|
|
130
|
+
Array.from(container.querySelectorAll("a")).some((link) =>
|
|
131
|
+
link.textContent?.includes("Responses"),
|
|
132
|
+
),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
102
136
|
it("exports visible table rows from the visible CSV download", async () => {
|
|
103
137
|
const container = document.createElement("div");
|
|
104
138
|
document.body.appendChild(container);
|