@agent-native/toolkit 0.6.0 → 0.7.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/README.md +17 -1
- package/dist/chat-history/ChatHistoryList.d.ts +103 -0
- package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryList.js +109 -0
- package/dist/chat-history/ChatHistoryList.js.map +1 -0
- package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
- package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryList.spec.js +283 -0
- package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
- package/dist/chat-history/index.d.ts +2 -0
- package/dist/chat-history/index.d.ts.map +1 -0
- package/dist/chat-history/index.js +2 -0
- package/dist/chat-history/index.js.map +1 -0
- package/dist/chat-history.css +314 -0
- package/package.json +17 -1
- package/src/app-shell/header-actions.tsx +84 -0
- package/src/app-shell/index.ts +1 -0
- package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
- package/src/chat-history/ChatHistoryList.tsx +469 -0
- package/src/chat-history/index.ts +7 -0
- package/src/chat-history.css +314 -0
- package/src/collab-ui/AgentPresenceChip.tsx +69 -0
- package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
- package/src/collab-ui/PresenceBar.tsx +346 -0
- package/src/collab-ui/RecentEditHighlights.tsx +225 -0
- package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
- package/src/collab-ui/agent-identity.ts +16 -0
- package/src/collab-ui/index.ts +39 -0
- package/src/collab-ui/lead-client.ts +32 -0
- package/src/collab-ui/types.spec.ts +18 -0
- package/src/collab-ui/types.ts +96 -0
- package/src/composer/AgentComposerFrame.tsx +59 -0
- package/src/composer/ComposerPlusMenu.tsx +983 -0
- package/src/composer/MentionPopover.tsx +412 -0
- package/src/composer/PastedTextChip.tsx +150 -0
- package/src/composer/PromptComposer.spec.ts +73 -0
- package/src/composer/PromptComposer.tsx +718 -0
- package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
- package/src/composer/RealtimeVoiceMode.tsx +856 -0
- package/src/composer/TiptapComposer.spec.ts +390 -0
- package/src/composer/TiptapComposer.tsx +2936 -0
- package/src/composer/VoiceButton.spec.ts +33 -0
- package/src/composer/VoiceButton.tsx +255 -0
- package/src/composer/asset-picker-url.spec.ts +56 -0
- package/src/composer/asset-picker-url.ts +80 -0
- package/src/composer/attachment-accept.spec.ts +22 -0
- package/src/composer/attachment-accept.ts +71 -0
- package/src/composer/draft-key.spec.ts +19 -0
- package/src/composer/draft-key.ts +7 -0
- package/src/composer/extensions/FileReference.tsx +55 -0
- package/src/composer/extensions/MentionReference.tsx +93 -0
- package/src/composer/extensions/SkillReference.tsx +56 -0
- package/src/composer/index.ts +79 -0
- package/src/composer/pasted-text.spec.ts +162 -0
- package/src/composer/pasted-text.ts +160 -0
- package/src/composer/prompt-attachments.spec.ts +80 -0
- package/src/composer/prompt-attachments.ts +93 -0
- package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
- package/src/composer/realtime-voice-audio-level.ts +64 -0
- package/src/composer/realtime-voice-transcript.spec.ts +95 -0
- package/src/composer/realtime-voice-transcript.ts +134 -0
- package/src/composer/runtime-adapters.tsx +343 -0
- package/src/composer/types.ts +66 -0
- package/src/composer/use-file-search.ts +64 -0
- package/src/composer/use-mention-search.ts +90 -0
- package/src/composer/use-skills.ts +45 -0
- package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
- package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
- package/src/composer/useVoiceDictation.spec.ts +45 -0
- package/src/composer/useVoiceDictation.ts +1191 -0
- package/src/context-ui/ContextMeter.tsx +101 -0
- package/src/context-ui/ContextSegmentRow.tsx +184 -0
- package/src/context-ui/ContextTreemap.tsx +125 -0
- package/src/context-ui/ContextXRayPanel.tsx +377 -0
- package/src/context-ui/format.ts +42 -0
- package/src/context-ui/index.ts +15 -0
- package/src/context-ui/types.ts +43 -0
- package/src/design-tweaks/index.ts +1 -0
- package/src/design-tweaks/visual-style-controls.tsx +1084 -0
- package/src/editor/BubbleToolbar.tsx +253 -0
- package/src/editor/CodeBlockNode.tsx +254 -0
- package/src/editor/DragHandle.spec.ts +588 -0
- package/src/editor/DragHandle.ts +1437 -0
- package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
- package/src/editor/ImageExtension.ts +306 -0
- package/src/editor/RegistryBlockContext.tsx +107 -0
- package/src/editor/RegistryBlockNode.spec.ts +279 -0
- package/src/editor/RegistryBlockNode.tsx +652 -0
- package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
- package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
- package/src/editor/RichMarkdownEditor.tsx +81 -0
- package/src/editor/RunId.ts +61 -0
- package/src/editor/SharedRichEditor.tsx +321 -0
- package/src/editor/SlashCommandMenu.tsx +310 -0
- package/src/editor/disableHistory.spec.ts +85 -0
- package/src/editor/extensions.ts +402 -0
- package/src/editor/gfmDoc.spec.ts +230 -0
- package/src/editor/gfmDoc.ts +92 -0
- package/src/editor/index.ts +93 -0
- package/src/editor/registrySlashCommands.ts +131 -0
- package/src/editor/structuralReorderUndo.spec.ts +144 -0
- package/src/editor/surgical-apply.spec.ts +279 -0
- package/src/editor/surgical-apply.ts +192 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
- package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
- package/src/editor/useCollabReconcile.ts +589 -0
- package/src/editor.css +510 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +188 -0
- package/src/index.ts +8 -0
- package/src/onboarding/index.ts +1 -0
- package/src/provider.tsx +47 -0
- package/src/sharing/VisibilityBadge.spec.ts +96 -0
- package/src/sharing/VisibilityBadge.tsx +37 -0
- package/src/sharing/index.ts +4 -0
- package/src/styles.css +21 -0
- package/src/ui/accordion.tsx +56 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/aspect-ratio.tsx +5 -0
- package/src/ui/avatar.tsx +48 -0
- package/src/ui/badge.tsx +37 -0
- package/src/ui/breadcrumb.tsx +115 -0
- package/src/ui/button.tsx +78 -0
- package/src/ui/calendar.tsx +214 -0
- package/src/ui/card.tsx +86 -0
- package/src/ui/carousel.tsx +260 -0
- package/src/ui/chart.tsx +375 -0
- package/src/ui/checkbox.tsx +28 -0
- package/src/ui/collapsible.tsx +9 -0
- package/src/ui/command.spec.tsx +51 -0
- package/src/ui/command.tsx +174 -0
- package/src/ui/context-menu.tsx +202 -0
- package/src/ui/date-picker.tsx +64 -0
- package/src/ui/dialog.spec.tsx +43 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/drawer.tsx +126 -0
- package/src/ui/dropdown-menu.tsx +218 -0
- package/src/ui/form.tsx +177 -0
- package/src/ui/hover-card.tsx +27 -0
- package/src/ui/index.ts +52 -0
- package/src/ui/input-otp.tsx +69 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/logo.tsx +36 -0
- package/src/ui/menubar.tsx +235 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.tsx +121 -0
- package/src/ui/popover.tsx +44 -0
- package/src/ui/progress.tsx +26 -0
- package/src/ui/radio-group.tsx +42 -0
- package/src/ui/resizable.tsx +43 -0
- package/src/ui/scroll-area.tsx +46 -0
- package/src/ui/select.tsx +158 -0
- package/src/ui/separator.tsx +29 -0
- package/src/ui/sheet.tsx +160 -0
- package/src/ui/sidebar.tsx +777 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/slider.tsx +26 -0
- package/src/ui/sonner.tsx +58 -0
- package/src/ui/spinner.tsx +17 -0
- package/src/ui/switch.tsx +27 -0
- package/src/ui/table.tsx +117 -0
- package/src/ui/tabs.tsx +53 -0
- package/src/ui/textarea.tsx +23 -0
- package/src/ui/toast.tsx +127 -0
- package/src/ui/toaster.tsx +33 -0
- package/src/ui/toggle-group.tsx +59 -0
- package/src/ui/toggle.tsx +43 -0
- package/src/ui/tooltip.tsx +53 -0
- package/src/ui/use-toast.ts +3 -0
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ChatHistoryList,
|
|
9
|
+
type ChatHistoryItem,
|
|
10
|
+
type ChatHistorySection,
|
|
11
|
+
} from "./ChatHistoryList.js";
|
|
12
|
+
|
|
13
|
+
function item(
|
|
14
|
+
overrides: Partial<ChatHistoryItem> & { id: string },
|
|
15
|
+
): ChatHistoryItem {
|
|
16
|
+
return {
|
|
17
|
+
title: overrides.id,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** React tracks the DOM input's value via a wrapped setter to decide whether
|
|
23
|
+
* to fire its synthetic change handler, so setting `.value` directly is not
|
|
24
|
+
* observed. Go through the native prototype setter instead, matching the
|
|
25
|
+
* pattern used by CommandMenu.spec.tsx. */
|
|
26
|
+
function typeIntoInput(input: HTMLInputElement, value: string) {
|
|
27
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
28
|
+
window.HTMLInputElement.prototype,
|
|
29
|
+
"value",
|
|
30
|
+
)?.set;
|
|
31
|
+
setter?.call(input, value);
|
|
32
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
33
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("ChatHistoryList", () => {
|
|
37
|
+
let container: HTMLDivElement;
|
|
38
|
+
let root: Root;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
42
|
+
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
|
43
|
+
cb(0);
|
|
44
|
+
return 0;
|
|
45
|
+
});
|
|
46
|
+
container = document.createElement("div");
|
|
47
|
+
document.body.appendChild(container);
|
|
48
|
+
root = createRoot(container);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
act(() => root.unmount());
|
|
53
|
+
container.remove();
|
|
54
|
+
document.body.innerHTML = "";
|
|
55
|
+
vi.restoreAllMocks();
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders a flat item list and calls onSelect when a row is clicked", () => {
|
|
60
|
+
const onSelect = vi.fn();
|
|
61
|
+
const items: ChatHistoryItem[] = [
|
|
62
|
+
item({ id: "thread-1", title: "First chat", timestamp: "Yesterday" }),
|
|
63
|
+
item({ id: "thread-2", title: "Second chat", timestamp: "2 min ago" }),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
act(() => {
|
|
67
|
+
root.render(<ChatHistoryList items={items} onSelect={onSelect} />);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const titles = Array.from(
|
|
71
|
+
container.querySelectorAll(".an-chat-history-row__title"),
|
|
72
|
+
).map((el) => el.textContent);
|
|
73
|
+
expect(titles).toEqual(["First chat", "Second chat"]);
|
|
74
|
+
expect(container.textContent).toContain("Yesterday");
|
|
75
|
+
expect(container.textContent).toContain("2 min ago");
|
|
76
|
+
|
|
77
|
+
const buttons = container.querySelectorAll(".an-chat-history-row__button");
|
|
78
|
+
act(() => {
|
|
79
|
+
(buttons[1] as HTMLButtonElement).click();
|
|
80
|
+
});
|
|
81
|
+
expect(onSelect).toHaveBeenCalledWith("thread-2");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("highlights the active item", () => {
|
|
85
|
+
const items: ChatHistoryItem[] = [
|
|
86
|
+
item({ id: "thread-1" }),
|
|
87
|
+
item({ id: "thread-2" }),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
act(() => {
|
|
91
|
+
root.render(
|
|
92
|
+
<ChatHistoryList
|
|
93
|
+
items={items}
|
|
94
|
+
activeId="thread-2"
|
|
95
|
+
onSelect={() => {}}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const rows = container.querySelectorAll(".an-chat-history-row");
|
|
101
|
+
expect(rows[0].className).not.toContain("an-chat-history-row--active");
|
|
102
|
+
expect(rows[1].className).toContain("an-chat-history-row--active");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders grouped sections with labels", () => {
|
|
106
|
+
const sections: ChatHistorySection[] = [
|
|
107
|
+
{
|
|
108
|
+
id: "scoped",
|
|
109
|
+
label: "This deck",
|
|
110
|
+
items: [item({ id: "thread-1", title: "Deck chat" })],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "other",
|
|
114
|
+
label: "All chats",
|
|
115
|
+
items: [item({ id: "thread-2", title: "Other chat" })],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
act(() => {
|
|
120
|
+
root.render(<ChatHistoryList sections={sections} onSelect={() => {}} />);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const labels = Array.from(
|
|
124
|
+
container.querySelectorAll(".an-chat-history__section-label"),
|
|
125
|
+
).map((el) => el.textContent);
|
|
126
|
+
expect(labels).toEqual(["This deck", "All chats"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("shows the empty state, distinguishing plain-empty from no-search-results", () => {
|
|
130
|
+
act(() => {
|
|
131
|
+
root.render(<ChatHistoryList items={[]} onSelect={() => {}} />);
|
|
132
|
+
});
|
|
133
|
+
expect(container.textContent).toContain("No chats yet");
|
|
134
|
+
|
|
135
|
+
act(() => {
|
|
136
|
+
root.render(
|
|
137
|
+
<ChatHistoryList
|
|
138
|
+
items={[]}
|
|
139
|
+
searchValue="foo"
|
|
140
|
+
onSearchChange={() => {}}
|
|
141
|
+
onSelect={() => {}}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
expect(container.textContent).toContain("No matching chats");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("shows a loading state instead of the list", () => {
|
|
149
|
+
const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
|
|
150
|
+
act(() => {
|
|
151
|
+
root.render(
|
|
152
|
+
<ChatHistoryList
|
|
153
|
+
items={items}
|
|
154
|
+
loading
|
|
155
|
+
loadingLabel="Searching..."
|
|
156
|
+
onSelect={() => {}}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
expect(container.textContent).toContain("Searching...");
|
|
161
|
+
expect(container.querySelector(".an-chat-history-row")).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("shows an error state that takes priority over loading and items", () => {
|
|
165
|
+
const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
|
|
166
|
+
act(() => {
|
|
167
|
+
root.render(
|
|
168
|
+
<ChatHistoryList
|
|
169
|
+
items={items}
|
|
170
|
+
loading
|
|
171
|
+
error="Could not load chats"
|
|
172
|
+
onSelect={() => {}}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
expect(container.textContent).toContain("Could not load chats");
|
|
177
|
+
expect(container.querySelector(".an-chat-history-row")).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("renders a controlled search box only when onSearchChange is supplied", () => {
|
|
181
|
+
const onSearchChange = vi.fn();
|
|
182
|
+
act(() => {
|
|
183
|
+
root.render(
|
|
184
|
+
<ChatHistoryList
|
|
185
|
+
items={[]}
|
|
186
|
+
searchValue="abc"
|
|
187
|
+
onSearchChange={onSearchChange}
|
|
188
|
+
onSelect={() => {}}
|
|
189
|
+
/>,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
193
|
+
".an-chat-history__search-input",
|
|
194
|
+
);
|
|
195
|
+
expect(input).not.toBeNull();
|
|
196
|
+
expect(input!.value).toBe("abc");
|
|
197
|
+
|
|
198
|
+
act(() => {
|
|
199
|
+
typeIntoInput(input!, "abcd");
|
|
200
|
+
});
|
|
201
|
+
expect(onSearchChange).toHaveBeenCalledWith("abcd");
|
|
202
|
+
|
|
203
|
+
act(() => {
|
|
204
|
+
root.render(<ChatHistoryList items={[]} onSelect={() => {}} />);
|
|
205
|
+
});
|
|
206
|
+
expect(
|
|
207
|
+
container.querySelector(".an-chat-history__search-input"),
|
|
208
|
+
).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("does not render a row action menu when no menu callbacks are given", () => {
|
|
212
|
+
const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
|
|
213
|
+
act(() => {
|
|
214
|
+
root.render(<ChatHistoryList items={items} onSelect={() => {}} />);
|
|
215
|
+
});
|
|
216
|
+
expect(container.querySelector(".an-chat-history-row__menu")).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("supports pin toggling via the row action menu", () => {
|
|
220
|
+
const onTogglePin = vi.fn();
|
|
221
|
+
const items: ChatHistoryItem[] = [
|
|
222
|
+
item({ id: "thread-1", title: "First chat" }),
|
|
223
|
+
];
|
|
224
|
+
act(() => {
|
|
225
|
+
root.render(
|
|
226
|
+
<ChatHistoryList
|
|
227
|
+
items={items}
|
|
228
|
+
onSelect={() => {}}
|
|
229
|
+
onTogglePin={onTogglePin}
|
|
230
|
+
/>,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const trigger = container.querySelector<HTMLButtonElement>(
|
|
235
|
+
".an-chat-history-row__menu-trigger",
|
|
236
|
+
);
|
|
237
|
+
expect(trigger).not.toBeNull();
|
|
238
|
+
act(() => {
|
|
239
|
+
trigger!.click();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const pinItem = Array.from(
|
|
243
|
+
container.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
244
|
+
).find((el) => el.textContent?.includes("Pin to top"));
|
|
245
|
+
expect(pinItem).toBeDefined();
|
|
246
|
+
act(() => {
|
|
247
|
+
(pinItem as HTMLButtonElement).click();
|
|
248
|
+
});
|
|
249
|
+
expect(onTogglePin).toHaveBeenCalledWith("thread-1");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("uses custom labels for localized row actions", () => {
|
|
253
|
+
const onTogglePin = vi.fn();
|
|
254
|
+
const onDelete = vi.fn();
|
|
255
|
+
const labels = {
|
|
256
|
+
options: "Conversation actions",
|
|
257
|
+
renameInput: "Rename analytics conversation",
|
|
258
|
+
rename: "Rename conversation",
|
|
259
|
+
pin: "Keep at top",
|
|
260
|
+
unpin: "Remove from top",
|
|
261
|
+
delete: "Archive conversation",
|
|
262
|
+
};
|
|
263
|
+
const render = (pinned = false) => {
|
|
264
|
+
act(() => {
|
|
265
|
+
root.render(
|
|
266
|
+
<ChatHistoryList
|
|
267
|
+
key={String(pinned)}
|
|
268
|
+
items={[item({ id: "thread-1", titleText: "Campaign", pinned })]}
|
|
269
|
+
onSelect={() => {}}
|
|
270
|
+
onRename={() => {}}
|
|
271
|
+
onTogglePin={onTogglePin}
|
|
272
|
+
onDelete={onDelete}
|
|
273
|
+
labels={labels}
|
|
274
|
+
/>,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
render();
|
|
280
|
+
const trigger = container.querySelector<HTMLButtonElement>(
|
|
281
|
+
".an-chat-history-row__menu-trigger",
|
|
282
|
+
);
|
|
283
|
+
expect(trigger?.getAttribute("aria-label")).toBe(labels.options);
|
|
284
|
+
act(() => {
|
|
285
|
+
trigger!.click();
|
|
286
|
+
});
|
|
287
|
+
expect(container.textContent).toContain(labels.rename);
|
|
288
|
+
expect(container.textContent).toContain(labels.pin);
|
|
289
|
+
expect(container.textContent).toContain(labels.delete);
|
|
290
|
+
|
|
291
|
+
const archiveItem = Array.from(
|
|
292
|
+
container.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
293
|
+
).find((element) => element.textContent?.includes(labels.delete));
|
|
294
|
+
act(() => {
|
|
295
|
+
(archiveItem as HTMLButtonElement).click();
|
|
296
|
+
});
|
|
297
|
+
expect(onDelete).toHaveBeenCalledWith("thread-1");
|
|
298
|
+
|
|
299
|
+
render();
|
|
300
|
+
const renameTrigger = container.querySelector<HTMLButtonElement>(
|
|
301
|
+
".an-chat-history-row__menu-trigger",
|
|
302
|
+
);
|
|
303
|
+
act(() => {
|
|
304
|
+
renameTrigger!.click();
|
|
305
|
+
});
|
|
306
|
+
const renameItem = Array.from(
|
|
307
|
+
container.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
308
|
+
).find((element) => element.textContent?.includes(labels.rename));
|
|
309
|
+
act(() => {
|
|
310
|
+
(renameItem as HTMLButtonElement).click();
|
|
311
|
+
});
|
|
312
|
+
expect(
|
|
313
|
+
container
|
|
314
|
+
.querySelector<HTMLInputElement>(".an-chat-history-row__rename-input")
|
|
315
|
+
?.getAttribute("aria-label"),
|
|
316
|
+
).toBe(labels.renameInput);
|
|
317
|
+
|
|
318
|
+
render(true);
|
|
319
|
+
const pinnedTrigger = container.querySelector<HTMLButtonElement>(
|
|
320
|
+
".an-chat-history-row__menu-trigger",
|
|
321
|
+
);
|
|
322
|
+
act(() => {
|
|
323
|
+
pinnedTrigger!.click();
|
|
324
|
+
});
|
|
325
|
+
expect(container.textContent).toContain(labels.unpin);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("supports inline rename via the row action menu", () => {
|
|
329
|
+
const onRename = vi.fn();
|
|
330
|
+
const items: ChatHistoryItem[] = [
|
|
331
|
+
item({ id: "thread-1", title: "Old title", titleText: "Old title" }),
|
|
332
|
+
];
|
|
333
|
+
act(() => {
|
|
334
|
+
root.render(
|
|
335
|
+
<ChatHistoryList
|
|
336
|
+
items={items}
|
|
337
|
+
onSelect={() => {}}
|
|
338
|
+
onRename={onRename}
|
|
339
|
+
renameMaxLength={160}
|
|
340
|
+
/>,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const trigger = container.querySelector<HTMLButtonElement>(
|
|
345
|
+
".an-chat-history-row__menu-trigger",
|
|
346
|
+
);
|
|
347
|
+
act(() => {
|
|
348
|
+
trigger!.click();
|
|
349
|
+
});
|
|
350
|
+
const renameItem = Array.from(
|
|
351
|
+
container.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
352
|
+
).find((el) => el.textContent?.includes("Rename"));
|
|
353
|
+
act(() => {
|
|
354
|
+
(renameItem as HTMLButtonElement).click();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
358
|
+
".an-chat-history-row__rename-input",
|
|
359
|
+
);
|
|
360
|
+
expect(input).not.toBeNull();
|
|
361
|
+
expect(input!.value).toBe("Old title");
|
|
362
|
+
expect(input!.maxLength).toBe(160);
|
|
363
|
+
|
|
364
|
+
act(() => {
|
|
365
|
+
typeIntoInput(input!, "New title");
|
|
366
|
+
});
|
|
367
|
+
act(() => {
|
|
368
|
+
input!.dispatchEvent(
|
|
369
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
expect(onRename).toHaveBeenCalledWith("thread-1", "New title");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("supports item-aware labels and app-specific actions", () => {
|
|
376
|
+
const onShare = vi.fn();
|
|
377
|
+
const items: ChatHistoryItem[] = [
|
|
378
|
+
item({ id: "thread-1", title: "Campaign", titleText: "Campaign" }),
|
|
379
|
+
];
|
|
380
|
+
act(() => {
|
|
381
|
+
root.render(
|
|
382
|
+
<ChatHistoryList
|
|
383
|
+
items={items}
|
|
384
|
+
onSelect={() => {}}
|
|
385
|
+
onRename={() => {}}
|
|
386
|
+
labels={{
|
|
387
|
+
options: (chat) => `Options for ${chat.titleText}`,
|
|
388
|
+
renameInput: (chat) => `Rename ${chat.titleText}`,
|
|
389
|
+
}}
|
|
390
|
+
renderAdditionalRowActions={(chat, closeMenu) => (
|
|
391
|
+
<button
|
|
392
|
+
type="button"
|
|
393
|
+
className="an-chat-history-row__menu-item"
|
|
394
|
+
onClick={() => {
|
|
395
|
+
closeMenu();
|
|
396
|
+
onShare(chat.id);
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
Share
|
|
400
|
+
</button>
|
|
401
|
+
)}
|
|
402
|
+
/>,
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const trigger = container.querySelector<HTMLButtonElement>(
|
|
407
|
+
".an-chat-history-row__menu-trigger",
|
|
408
|
+
);
|
|
409
|
+
expect(trigger?.getAttribute("aria-label")).toBe("Options for Campaign");
|
|
410
|
+
act(() => {
|
|
411
|
+
trigger!.click();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const shareItem = Array.from(
|
|
415
|
+
container.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
416
|
+
).find((element) => element.textContent === "Share");
|
|
417
|
+
act(() => {
|
|
418
|
+
(shareItem as HTMLButtonElement).click();
|
|
419
|
+
});
|
|
420
|
+
expect(onShare).toHaveBeenCalledWith("thread-1");
|
|
421
|
+
expect(
|
|
422
|
+
container.querySelector(".an-chat-history-row__menu-content"),
|
|
423
|
+
).toBeNull();
|
|
424
|
+
|
|
425
|
+
act(() => {
|
|
426
|
+
trigger!.click();
|
|
427
|
+
});
|
|
428
|
+
const renameItem = Array.from(
|
|
429
|
+
container.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
430
|
+
).find((element) => element.textContent === "Rename");
|
|
431
|
+
act(() => {
|
|
432
|
+
(renameItem as HTMLButtonElement).click();
|
|
433
|
+
});
|
|
434
|
+
expect(
|
|
435
|
+
container
|
|
436
|
+
.querySelector<HTMLInputElement>(".an-chat-history-row__rename-input")
|
|
437
|
+
?.getAttribute("aria-label"),
|
|
438
|
+
).toBe("Rename Campaign");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("renders the footer inside the scroll region regardless of list state", () => {
|
|
442
|
+
act(() => {
|
|
443
|
+
root.render(
|
|
444
|
+
<ChatHistoryList
|
|
445
|
+
items={[]}
|
|
446
|
+
onSelect={() => {}}
|
|
447
|
+
footer={<div data-testid="footer">Load older chats</div>}
|
|
448
|
+
/>,
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
expect(container.textContent).toContain("Load older chats");
|
|
452
|
+
});
|
|
453
|
+
});
|