@copilotkit/react-core 1.54.1-next.6 → 1.55.0-next.7
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 +121 -106
- package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
- package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
- package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
- package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
- package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
- package/dist/index.cjs +27 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1941 -35
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +77 -7
- package/dist/v2/index.css +1 -2
- package/dist/v2/index.d.cts +6 -4
- package/dist/v2/index.d.mts +6 -4
- package/dist/v2/index.mjs +7 -4
- package/dist/v2/index.umd.js +5725 -24
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +35 -7
- package/scripts/scope-preflight.mjs +101 -0
- package/src/components/CopilotListeners.tsx +2 -6
- package/src/components/copilot-provider/copilot-messages.tsx +1 -1
- package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
- package/src/components/copilot-provider/copilotkit.tsx +4 -4
- package/src/context/copilot-messages-context.tsx +1 -1
- package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
- package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
- package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
- package/src/hooks/use-agent-nodename.ts +1 -1
- package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
- package/src/hooks/use-coagent.ts +1 -1
- package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat_internal.ts +2 -2
- package/src/hooks/use-copilot-readable.ts +1 -1
- package/src/hooks/use-frontend-tool.ts +2 -2
- package/src/hooks/use-human-in-the-loop.ts +2 -2
- package/src/hooks/use-langgraph-interrupt.ts +2 -5
- package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
- package/src/hooks/use-render-tool-call.ts +1 -1
- package/src/lib/copilot-task.ts +1 -1
- package/src/setupTests.ts +18 -14
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
- package/src/v2/__tests__/globalSetup.ts +14 -0
- package/src/v2/__tests__/setup.ts +93 -0
- package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
- package/src/v2/components/CopilotKitInspector.tsx +50 -0
- package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
- package/src/v2/components/WildcardToolCallRender.tsx +86 -0
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
- package/src/v2/components/chat/CopilotChat.tsx +431 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
- package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
- package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
- package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
- package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
- package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
- package/src/v2/components/chat/CopilotChatView.tsx +598 -0
- package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
- package/src/v2/components/chat/CopilotPopup.tsx +81 -0
- package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
- package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
- package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
- package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
- package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
- package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
- package/src/v2/components/chat/__tests__/setup.ts +1 -0
- package/src/v2/components/chat/index.ts +79 -0
- package/src/v2/components/index.ts +7 -0
- package/src/v2/components/license-warning-banner.tsx +198 -0
- package/src/v2/components/ui/button.tsx +123 -0
- package/src/v2/components/ui/dropdown-menu.tsx +258 -0
- package/src/v2/components/ui/tooltip.tsx +60 -0
- package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
- package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
- package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
- package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
- package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
- package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
- package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
- package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
- package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
- package/src/v2/hooks/index.ts +18 -0
- package/src/v2/hooks/use-agent-context.tsx +45 -0
- package/src/v2/hooks/use-agent.tsx +155 -0
- package/src/v2/hooks/use-component.tsx +89 -0
- package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
- package/src/v2/hooks/use-default-render-tool.tsx +254 -0
- package/src/v2/hooks/use-frontend-tool.tsx +43 -0
- package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
- package/src/v2/hooks/use-interrupt.tsx +305 -0
- package/src/v2/hooks/use-keyboard-height.tsx +67 -0
- package/src/v2/hooks/use-render-activity-message.tsx +73 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
- package/src/v2/hooks/use-render-tool-call.tsx +175 -0
- package/src/v2/hooks/use-render-tool.tsx +181 -0
- package/src/v2/hooks/use-suggestions.tsx +91 -0
- package/src/v2/hooks/use-threads.tsx +256 -0
- package/src/v2/hooks/useKatexStyles.ts +27 -0
- package/src/v2/index.css +1 -1
- package/src/v2/index.ts +18 -2
- package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
- package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
- package/src/v2/lib/react-core.ts +156 -0
- package/src/v2/lib/slots.tsx +143 -0
- package/src/v2/lib/transcription-client.ts +184 -0
- package/src/v2/lib/utils.ts +8 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
- package/src/v2/providers/CopilotKitProvider.tsx +600 -0
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
- package/src/v2/providers/index.ts +14 -0
- package/src/v2/styles/globals.css +230 -0
- package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
- package/src/v2/types/defineToolCallRenderer.ts +65 -0
- package/src/v2/types/frontend-tool.ts +8 -0
- package/src/v2/types/human-in-the-loop.ts +33 -0
- package/src/v2/types/index.ts +7 -0
- package/src/v2/types/interrupt.ts +15 -0
- package/src/v2/types/react-activity-message-renderer.ts +27 -0
- package/src/v2/types/react-custom-message-renderer.ts +17 -0
- package/src/v2/types/react-tool-call-renderer.ts +32 -0
- package/tsdown.config.ts +34 -10
- package/vitest.config.mjs +4 -3
- package/LICENSE +0 -21
- package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
- package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
- package/dist/copilotkit-C94ayZbs.cjs +0 -2161
- package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
- package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
- package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
- package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
- package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
- package/dist/v2/index.css.map +0 -1
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
|
+
import { CopilotChatView } from "../CopilotChatView";
|
|
5
|
+
import { CopilotChatMessageView } from "../CopilotChatMessageView";
|
|
6
|
+
import { CopilotChatInput } from "../CopilotChatInput";
|
|
7
|
+
import { CopilotChatSuggestionView } from "../CopilotChatSuggestionView";
|
|
8
|
+
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
|
|
9
|
+
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
|
|
10
|
+
|
|
11
|
+
// Wrapper to provide required context
|
|
12
|
+
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
13
|
+
<CopilotKitProvider>
|
|
14
|
+
<CopilotChatConfigurationProvider threadId="test-thread">
|
|
15
|
+
<div style={{ height: 400 }}>{children}</div>
|
|
16
|
+
</CopilotChatConfigurationProvider>
|
|
17
|
+
</CopilotKitProvider>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const sampleMessages = [
|
|
21
|
+
{ id: "1", role: "user" as const, content: "Hello" },
|
|
22
|
+
{ id: "2", role: "assistant" as const, content: "Hi there!" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
describe("CopilotChatView Slot System E2E Tests", () => {
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// 1. TAILWIND CLASS TESTS
|
|
28
|
+
// ============================================================================
|
|
29
|
+
describe("1. Tailwind Class Slot Override", () => {
|
|
30
|
+
describe("messageView slot", () => {
|
|
31
|
+
it("should apply tailwind class string to messageView", () => {
|
|
32
|
+
render(
|
|
33
|
+
<TestWrapper>
|
|
34
|
+
<CopilotChatView
|
|
35
|
+
messages={sampleMessages}
|
|
36
|
+
messageView="bg-red-500 text-white p-4"
|
|
37
|
+
/>
|
|
38
|
+
</TestWrapper>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// The messageView should have the custom tailwind classes
|
|
42
|
+
const messageContainer = document.querySelector(
|
|
43
|
+
'[class*="bg-red-500"]',
|
|
44
|
+
);
|
|
45
|
+
expect(messageContainer).toBeDefined();
|
|
46
|
+
expect(messageContainer?.classList.contains("text-white")).toBe(true);
|
|
47
|
+
expect(messageContainer?.classList.contains("p-4")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should override default className with tailwind string", () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<TestWrapper>
|
|
53
|
+
<CopilotChatView
|
|
54
|
+
messages={sampleMessages}
|
|
55
|
+
messageView="custom-override-class"
|
|
56
|
+
/>
|
|
57
|
+
</TestWrapper>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const messageContainer = container.querySelector(
|
|
61
|
+
".custom-override-class",
|
|
62
|
+
);
|
|
63
|
+
expect(messageContainer).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("scrollView slot", () => {
|
|
68
|
+
it("should apply tailwind class string to scrollView", () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<TestWrapper>
|
|
71
|
+
<CopilotChatView
|
|
72
|
+
messages={sampleMessages}
|
|
73
|
+
scrollView="overflow-y-auto bg-gray-100"
|
|
74
|
+
/>
|
|
75
|
+
</TestWrapper>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const scrollContainer = container.querySelector(".overflow-y-auto");
|
|
79
|
+
expect(scrollContainer).toBeDefined();
|
|
80
|
+
expect(scrollContainer?.classList.contains("bg-gray-100")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("scrollToBottomButton slot (nested under scrollView)", () => {
|
|
85
|
+
it("should apply tailwind class string to scrollToBottomButton via scrollView", () => {
|
|
86
|
+
const { container } = render(
|
|
87
|
+
<TestWrapper>
|
|
88
|
+
<CopilotChatView
|
|
89
|
+
messages={sampleMessages}
|
|
90
|
+
scrollView={{ scrollToBottomButton: "bg-blue-500 rounded-full" }}
|
|
91
|
+
/>
|
|
92
|
+
</TestWrapper>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Note: button may only appear when scrolled
|
|
96
|
+
const button = container.querySelector(".bg-blue-500");
|
|
97
|
+
if (button) {
|
|
98
|
+
expect(button.classList.contains("rounded-full")).toBe(true);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("input slot", () => {
|
|
104
|
+
it("should apply tailwind class string to input", () => {
|
|
105
|
+
const { container } = render(
|
|
106
|
+
<TestWrapper>
|
|
107
|
+
<CopilotChatView
|
|
108
|
+
messages={sampleMessages}
|
|
109
|
+
input="border-2 border-purple-500"
|
|
110
|
+
/>
|
|
111
|
+
</TestWrapper>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const inputContainer = container.querySelector(".border-purple-500");
|
|
115
|
+
expect(inputContainer).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("feather slot (via scrollView)", () => {
|
|
120
|
+
it("should apply tailwind class string to feather via scrollView", () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<TestWrapper>
|
|
123
|
+
<CopilotChatView
|
|
124
|
+
messages={sampleMessages}
|
|
125
|
+
scrollView={{ feather: "text-green-500 font-bold" }}
|
|
126
|
+
/>
|
|
127
|
+
</TestWrapper>,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const feather = container.querySelector(".text-green-500");
|
|
131
|
+
if (feather) {
|
|
132
|
+
expect(feather.classList.contains("font-bold")).toBe(true);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("suggestionView slot", () => {
|
|
138
|
+
it("should apply tailwind class string to suggestionView", () => {
|
|
139
|
+
const suggestions = [
|
|
140
|
+
{ title: "Test", message: "Test message", isLoading: false },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const { container } = render(
|
|
144
|
+
<TestWrapper>
|
|
145
|
+
<CopilotChatView
|
|
146
|
+
messages={sampleMessages}
|
|
147
|
+
suggestions={suggestions}
|
|
148
|
+
suggestionView="flex gap-2 bg-indigo-50"
|
|
149
|
+
/>
|
|
150
|
+
</TestWrapper>,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const suggestionContainer = container.querySelector(".bg-indigo-50");
|
|
154
|
+
expect(suggestionContainer).toBeDefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("className vs tailwind string precedence", () => {
|
|
159
|
+
it("tailwind string should completely replace className (not merge)", () => {
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<TestWrapper>
|
|
162
|
+
<CopilotChatView
|
|
163
|
+
messages={sampleMessages}
|
|
164
|
+
input="only-this-class"
|
|
165
|
+
/>
|
|
166
|
+
</TestWrapper>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const inputEl = container.querySelector(".only-this-class");
|
|
170
|
+
expect(inputEl).toBeDefined();
|
|
171
|
+
// The string replaces className, so default classes should not be present
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("non-tailwind inline styles should still work", () => {
|
|
176
|
+
it("should accept style prop alongside className override", () => {
|
|
177
|
+
const CustomInput = React.forwardRef<HTMLDivElement, any>(
|
|
178
|
+
(props, ref) => (
|
|
179
|
+
<div
|
|
180
|
+
ref={ref}
|
|
181
|
+
className={props.className}
|
|
182
|
+
style={{ backgroundColor: "rgb(255, 0, 0)" }}
|
|
183
|
+
data-testid="custom-input"
|
|
184
|
+
>
|
|
185
|
+
{props.children}
|
|
186
|
+
</div>
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
CustomInput.displayName = "CustomInput";
|
|
190
|
+
|
|
191
|
+
render(
|
|
192
|
+
<TestWrapper>
|
|
193
|
+
<CopilotChatView messages={sampleMessages} input={CustomInput} />
|
|
194
|
+
</TestWrapper>,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const customInput = screen.queryByTestId("custom-input");
|
|
198
|
+
if (customInput) {
|
|
199
|
+
expect(customInput.style.backgroundColor).toBe("rgb(255, 0, 0)");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// 2. PROPERTIES (onClick, disabled, etc.) TESTS
|
|
207
|
+
// ============================================================================
|
|
208
|
+
describe("2. Properties Slot Override", () => {
|
|
209
|
+
describe("scrollToBottomButton props (nested under scrollView)", () => {
|
|
210
|
+
it("should pass onClick handler to scrollToBottomButton via scrollView", () => {
|
|
211
|
+
const handleClick = vi.fn();
|
|
212
|
+
|
|
213
|
+
render(
|
|
214
|
+
<TestWrapper>
|
|
215
|
+
<CopilotChatView
|
|
216
|
+
messages={sampleMessages}
|
|
217
|
+
scrollView={{ scrollToBottomButton: { onClick: handleClick } }}
|
|
218
|
+
/>
|
|
219
|
+
</TestWrapper>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Find and click the scroll button if visible
|
|
223
|
+
const buttons = document.querySelectorAll("button");
|
|
224
|
+
buttons.forEach((btn) => {
|
|
225
|
+
if (btn.getAttribute("aria-label")?.includes("scroll")) {
|
|
226
|
+
fireEvent.click(btn);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// Note: onClick may only fire if button is visible
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should pass disabled prop to scrollToBottomButton via scrollView", () => {
|
|
233
|
+
const { container } = render(
|
|
234
|
+
<TestWrapper>
|
|
235
|
+
<CopilotChatView
|
|
236
|
+
messages={sampleMessages}
|
|
237
|
+
scrollView={{ scrollToBottomButton: { disabled: true } }}
|
|
238
|
+
/>
|
|
239
|
+
</TestWrapper>,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const buttons = container.querySelectorAll("button[disabled]");
|
|
243
|
+
// Check if any button is disabled
|
|
244
|
+
expect(buttons).toBeDefined();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("input props", () => {
|
|
249
|
+
it("should pass onFocus handler to input", async () => {
|
|
250
|
+
const handleFocus = vi.fn();
|
|
251
|
+
|
|
252
|
+
render(
|
|
253
|
+
<TestWrapper>
|
|
254
|
+
<CopilotChatView
|
|
255
|
+
messages={sampleMessages}
|
|
256
|
+
input={{ onFocus: handleFocus }}
|
|
257
|
+
/>
|
|
258
|
+
</TestWrapper>,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const textarea = await screen.findByRole("textbox");
|
|
262
|
+
fireEvent.focus(textarea);
|
|
263
|
+
// Note: depends on how input passes props through
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should pass autoFocus prop to input", () => {
|
|
267
|
+
render(
|
|
268
|
+
<TestWrapper>
|
|
269
|
+
<CopilotChatView
|
|
270
|
+
messages={sampleMessages}
|
|
271
|
+
input={{ autoFocus: true }}
|
|
272
|
+
/>
|
|
273
|
+
</TestWrapper>,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Check if textbox is focused
|
|
277
|
+
const textarea = document.querySelector("textarea");
|
|
278
|
+
// autoFocus behavior may vary
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("messageView props", () => {
|
|
283
|
+
it("should pass isRunning prop to messageView", () => {
|
|
284
|
+
render(
|
|
285
|
+
<TestWrapper>
|
|
286
|
+
<CopilotChatView
|
|
287
|
+
messages={sampleMessages}
|
|
288
|
+
isRunning={true}
|
|
289
|
+
messageView={{ "data-testid": "message-view-running" } as any}
|
|
290
|
+
/>
|
|
291
|
+
</TestWrapper>,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const messageView = screen.queryByTestId("message-view-running");
|
|
295
|
+
expect(messageView).toBeDefined();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// 3. CUSTOM COMPONENT TESTS
|
|
302
|
+
// ============================================================================
|
|
303
|
+
describe("3. Custom Component Slot Override", () => {
|
|
304
|
+
describe("messageView custom component", () => {
|
|
305
|
+
it("should render custom messageView component", () => {
|
|
306
|
+
const CustomMessageView: React.FC<any> = ({ messages }) => (
|
|
307
|
+
<div data-testid="custom-message-view">
|
|
308
|
+
Custom: {messages?.length || 0} messages
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
render(
|
|
313
|
+
<TestWrapper>
|
|
314
|
+
<CopilotChatView
|
|
315
|
+
messages={sampleMessages}
|
|
316
|
+
messageView={CustomMessageView}
|
|
317
|
+
/>
|
|
318
|
+
</TestWrapper>,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
expect(screen.getByTestId("custom-message-view")).toBeDefined();
|
|
322
|
+
expect(screen.getByText(/Custom: 2 messages/)).toBeDefined();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("custom messageView should receive all props including messages", () => {
|
|
326
|
+
const receivedProps: any = {};
|
|
327
|
+
|
|
328
|
+
const CustomMessageView: React.FC<any> = (props) => {
|
|
329
|
+
Object.assign(receivedProps, props);
|
|
330
|
+
return <div data-testid="props-receiver">Received</div>;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
render(
|
|
334
|
+
<TestWrapper>
|
|
335
|
+
<CopilotChatView
|
|
336
|
+
messages={sampleMessages}
|
|
337
|
+
isRunning={true}
|
|
338
|
+
messageView={CustomMessageView}
|
|
339
|
+
/>
|
|
340
|
+
</TestWrapper>,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(receivedProps.messages).toBeDefined();
|
|
344
|
+
expect(receivedProps.messages.length).toBe(2);
|
|
345
|
+
expect(receivedProps.isRunning).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("input custom component", () => {
|
|
350
|
+
it("should render custom input component", () => {
|
|
351
|
+
const CustomInput: React.FC<any> = (props) => (
|
|
352
|
+
<div data-testid="custom-input">
|
|
353
|
+
<input
|
|
354
|
+
type="text"
|
|
355
|
+
placeholder="Custom input"
|
|
356
|
+
onChange={(e) => props.onChange?.(e.target.value)}
|
|
357
|
+
/>
|
|
358
|
+
<button onClick={() => props.onSubmitMessage?.("test")}>
|
|
359
|
+
Send
|
|
360
|
+
</button>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
render(
|
|
365
|
+
<TestWrapper>
|
|
366
|
+
<CopilotChatView messages={sampleMessages} input={CustomInput} />
|
|
367
|
+
</TestWrapper>,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(screen.getByTestId("custom-input")).toBeDefined();
|
|
371
|
+
expect(screen.getByPlaceholderText("Custom input")).toBeDefined();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("custom input should receive onSubmitMessage callback", () => {
|
|
375
|
+
const submitHandler = vi.fn();
|
|
376
|
+
|
|
377
|
+
const CustomInput: React.FC<any> = ({ onSubmitMessage }) => (
|
|
378
|
+
<button
|
|
379
|
+
data-testid="custom-submit"
|
|
380
|
+
onClick={() => onSubmitMessage?.("test message")}
|
|
381
|
+
>
|
|
382
|
+
Submit
|
|
383
|
+
</button>
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
render(
|
|
387
|
+
<TestWrapper>
|
|
388
|
+
<CopilotChatView
|
|
389
|
+
messages={sampleMessages}
|
|
390
|
+
input={CustomInput}
|
|
391
|
+
onSubmitMessage={submitHandler}
|
|
392
|
+
/>
|
|
393
|
+
</TestWrapper>,
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
fireEvent.click(screen.getByTestId("custom-submit"));
|
|
397
|
+
expect(submitHandler).toHaveBeenCalledWith("test message");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("scrollView custom component", () => {
|
|
402
|
+
it("should render custom scrollView component", () => {
|
|
403
|
+
const CustomScrollView: React.FC<any> = ({ children }) => (
|
|
404
|
+
<div
|
|
405
|
+
data-testid="custom-scroll"
|
|
406
|
+
style={{ maxHeight: 300, overflow: "auto" }}
|
|
407
|
+
>
|
|
408
|
+
{children}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
render(
|
|
413
|
+
<TestWrapper>
|
|
414
|
+
<CopilotChatView
|
|
415
|
+
messages={sampleMessages}
|
|
416
|
+
scrollView={CustomScrollView}
|
|
417
|
+
/>
|
|
418
|
+
</TestWrapper>,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
expect(screen.getByTestId("custom-scroll")).toBeDefined();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("suggestionView custom component", () => {
|
|
426
|
+
it("should render custom suggestionView component", () => {
|
|
427
|
+
const suggestions = [
|
|
428
|
+
{ title: "Option A", message: "Do A", isLoading: false },
|
|
429
|
+
{ title: "Option B", message: "Do B", isLoading: false },
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
const CustomSuggestionView: React.FC<any> = ({
|
|
433
|
+
suggestions,
|
|
434
|
+
onSelectSuggestion,
|
|
435
|
+
}) => (
|
|
436
|
+
<div data-testid="custom-suggestions">
|
|
437
|
+
{suggestions.map((s: any, i: number) => (
|
|
438
|
+
<button key={i} onClick={() => onSelectSuggestion?.(s, i)}>
|
|
439
|
+
{s.title}
|
|
440
|
+
</button>
|
|
441
|
+
))}
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
render(
|
|
446
|
+
<TestWrapper>
|
|
447
|
+
<CopilotChatView
|
|
448
|
+
messages={sampleMessages}
|
|
449
|
+
suggestions={suggestions}
|
|
450
|
+
suggestionView={CustomSuggestionView}
|
|
451
|
+
/>
|
|
452
|
+
</TestWrapper>,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
expect(screen.getByTestId("custom-suggestions")).toBeDefined();
|
|
456
|
+
expect(screen.getByText("Option A")).toBeDefined();
|
|
457
|
+
expect(screen.getByText("Option B")).toBeDefined();
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("feather custom component (via scrollView)", () => {
|
|
462
|
+
it("should render custom feather component via scrollView", () => {
|
|
463
|
+
const CustomFeather: React.FC<any> = () => (
|
|
464
|
+
<div data-testid="custom-feather">Custom Feather</div>
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
render(
|
|
468
|
+
<TestWrapper>
|
|
469
|
+
<CopilotChatView
|
|
470
|
+
messages={sampleMessages}
|
|
471
|
+
scrollView={{ feather: CustomFeather }}
|
|
472
|
+
/>
|
|
473
|
+
</TestWrapper>,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const feather = screen.queryByTestId("custom-feather");
|
|
477
|
+
if (feather) {
|
|
478
|
+
expect(feather.textContent).toContain("Custom Feather");
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe("scrollToBottomButton custom component (nested under scrollView)", () => {
|
|
484
|
+
it("should render custom scrollToBottomButton component via scrollView", () => {
|
|
485
|
+
const CustomScrollButton: React.FC<any> = ({ onClick }) => (
|
|
486
|
+
<button data-testid="custom-scroll-btn" onClick={onClick}>
|
|
487
|
+
⬇️ Go Down
|
|
488
|
+
</button>
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
render(
|
|
492
|
+
<TestWrapper>
|
|
493
|
+
<CopilotChatView
|
|
494
|
+
messages={sampleMessages}
|
|
495
|
+
scrollView={{ scrollToBottomButton: CustomScrollButton }}
|
|
496
|
+
/>
|
|
497
|
+
</TestWrapper>,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Button may only appear when scrolled
|
|
501
|
+
const btn = screen.queryByTestId("custom-scroll-btn");
|
|
502
|
+
if (btn) {
|
|
503
|
+
expect(btn.textContent).toContain("Go Down");
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// ============================================================================
|
|
510
|
+
// 4. RECURSIVE DRILL-DOWN TESTS
|
|
511
|
+
// ============================================================================
|
|
512
|
+
describe("4. Recursive Subcomponent Drill-Down", () => {
|
|
513
|
+
describe("messageView -> assistantMessage drill-down", () => {
|
|
514
|
+
it("should allow customizing assistantMessage within messageView", () => {
|
|
515
|
+
const CustomAssistantMessage: React.FC<any> = ({ message }) => (
|
|
516
|
+
<div data-testid="custom-assistant-msg">
|
|
517
|
+
Custom Assistant: {message?.content}
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
render(
|
|
522
|
+
<TestWrapper>
|
|
523
|
+
<CopilotChatView
|
|
524
|
+
messages={sampleMessages}
|
|
525
|
+
messageView={{
|
|
526
|
+
assistantMessage: CustomAssistantMessage,
|
|
527
|
+
}}
|
|
528
|
+
/>
|
|
529
|
+
</TestWrapper>,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
expect(screen.getByTestId("custom-assistant-msg")).toBeDefined();
|
|
533
|
+
expect(screen.getByText(/Custom Assistant: Hi there!/)).toBeDefined();
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe("messageView -> userMessage drill-down", () => {
|
|
538
|
+
it("should allow customizing userMessage within messageView", () => {
|
|
539
|
+
const CustomUserMessage: React.FC<any> = ({ message }) => (
|
|
540
|
+
<div data-testid="custom-user-msg">User said: {message?.content}</div>
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
render(
|
|
544
|
+
<TestWrapper>
|
|
545
|
+
<CopilotChatView
|
|
546
|
+
messages={sampleMessages}
|
|
547
|
+
messageView={{
|
|
548
|
+
userMessage: CustomUserMessage,
|
|
549
|
+
}}
|
|
550
|
+
/>
|
|
551
|
+
</TestWrapper>,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
expect(screen.getByTestId("custom-user-msg")).toBeDefined();
|
|
555
|
+
expect(screen.getByText(/User said: Hello/)).toBeDefined();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
describe("messageView -> cursor drill-down", () => {
|
|
560
|
+
it("should allow customizing cursor within messageView", () => {
|
|
561
|
+
const CustomCursor: React.FC<any> = () => (
|
|
562
|
+
<span data-testid="custom-cursor">|</span>
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
render(
|
|
566
|
+
<TestWrapper>
|
|
567
|
+
<CopilotChatView
|
|
568
|
+
messages={sampleMessages}
|
|
569
|
+
isRunning={true}
|
|
570
|
+
messageView={{
|
|
571
|
+
cursor: CustomCursor,
|
|
572
|
+
}}
|
|
573
|
+
/>
|
|
574
|
+
</TestWrapper>,
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// Cursor appears when running
|
|
578
|
+
const cursor = screen.queryByTestId("custom-cursor");
|
|
579
|
+
if (cursor) {
|
|
580
|
+
expect(cursor.textContent).toBe("|");
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe("input -> textArea drill-down", () => {
|
|
586
|
+
it("should allow customizing textArea within input", () => {
|
|
587
|
+
const CustomTextArea: React.FC<any> = React.forwardRef<
|
|
588
|
+
HTMLTextAreaElement,
|
|
589
|
+
any
|
|
590
|
+
>(({ value, onChange, ...props }, ref) => (
|
|
591
|
+
<textarea
|
|
592
|
+
ref={ref}
|
|
593
|
+
data-testid="custom-textarea"
|
|
594
|
+
value={value}
|
|
595
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
596
|
+
placeholder="Type here..."
|
|
597
|
+
{...props}
|
|
598
|
+
/>
|
|
599
|
+
));
|
|
600
|
+
|
|
601
|
+
render(
|
|
602
|
+
<TestWrapper>
|
|
603
|
+
<CopilotChatView
|
|
604
|
+
messages={sampleMessages}
|
|
605
|
+
input={{
|
|
606
|
+
textArea: CustomTextArea,
|
|
607
|
+
}}
|
|
608
|
+
/>
|
|
609
|
+
</TestWrapper>,
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const textarea = screen.queryByTestId("custom-textarea");
|
|
613
|
+
if (textarea) {
|
|
614
|
+
expect(textarea.getAttribute("placeholder")).toBe("Type here...");
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe("input -> sendButton drill-down", () => {
|
|
620
|
+
it("should allow customizing sendButton within input", () => {
|
|
621
|
+
const CustomSendButton: React.FC<any> = ({ onClick, disabled }) => (
|
|
622
|
+
<button
|
|
623
|
+
data-testid="custom-send-btn"
|
|
624
|
+
onClick={onClick}
|
|
625
|
+
disabled={disabled}
|
|
626
|
+
>
|
|
627
|
+
🚀 Send
|
|
628
|
+
</button>
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
render(
|
|
632
|
+
<TestWrapper>
|
|
633
|
+
<CopilotChatView
|
|
634
|
+
messages={sampleMessages}
|
|
635
|
+
input={{
|
|
636
|
+
sendButton: CustomSendButton,
|
|
637
|
+
}}
|
|
638
|
+
/>
|
|
639
|
+
</TestWrapper>,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
const sendBtn = screen.queryByTestId("custom-send-btn");
|
|
643
|
+
if (sendBtn) {
|
|
644
|
+
expect(sendBtn.textContent).toContain("Send");
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
describe("input -> addMenuButton drill-down", () => {
|
|
650
|
+
it("should allow customizing addMenuButton within input", () => {
|
|
651
|
+
const CustomAddMenu: React.FC<any> = () => (
|
|
652
|
+
<button data-testid="custom-add-menu">+ Add</button>
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
render(
|
|
656
|
+
<TestWrapper>
|
|
657
|
+
<CopilotChatView
|
|
658
|
+
messages={sampleMessages}
|
|
659
|
+
input={{
|
|
660
|
+
addMenuButton: CustomAddMenu,
|
|
661
|
+
}}
|
|
662
|
+
/>
|
|
663
|
+
</TestWrapper>,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const addBtn = screen.queryByTestId("custom-add-menu");
|
|
667
|
+
if (addBtn) {
|
|
668
|
+
expect(addBtn.textContent).toContain("Add");
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe("suggestionView -> suggestion drill-down", () => {
|
|
674
|
+
it("should allow customizing suggestion pill within suggestionView", () => {
|
|
675
|
+
const suggestions = [
|
|
676
|
+
{ title: "Suggestion 1", message: "Do 1", isLoading: false },
|
|
677
|
+
];
|
|
678
|
+
|
|
679
|
+
// Custom suggestion component that handles the props passed by the slot system
|
|
680
|
+
const CustomSuggestionPill: React.FC<any> = ({
|
|
681
|
+
suggestion,
|
|
682
|
+
onClick,
|
|
683
|
+
title,
|
|
684
|
+
}) => (
|
|
685
|
+
<button
|
|
686
|
+
data-testid="custom-suggestion-pill"
|
|
687
|
+
onClick={() => onClick?.(suggestion)}
|
|
688
|
+
>
|
|
689
|
+
💡 {suggestion?.title ?? title ?? "Suggestion"}
|
|
690
|
+
</button>
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
render(
|
|
694
|
+
<TestWrapper>
|
|
695
|
+
<CopilotChatView
|
|
696
|
+
messages={sampleMessages}
|
|
697
|
+
suggestions={suggestions}
|
|
698
|
+
suggestionView={{
|
|
699
|
+
suggestion: CustomSuggestionPill,
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
</TestWrapper>,
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
const pill = screen.queryByTestId("custom-suggestion-pill");
|
|
706
|
+
if (pill) {
|
|
707
|
+
expect(pill.textContent).toContain("Suggestion");
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe("suggestionView -> container drill-down", () => {
|
|
713
|
+
it("should allow customizing container within suggestionView", () => {
|
|
714
|
+
const suggestions = [
|
|
715
|
+
{ title: "Test", message: "Test msg", isLoading: false },
|
|
716
|
+
];
|
|
717
|
+
|
|
718
|
+
const CustomContainer: React.FC<any> = React.forwardRef<
|
|
719
|
+
HTMLDivElement,
|
|
720
|
+
any
|
|
721
|
+
>(({ children, ...props }, ref) => (
|
|
722
|
+
<div ref={ref} data-testid="custom-suggestion-container" {...props}>
|
|
723
|
+
<span>Suggestions:</span>
|
|
724
|
+
{children}
|
|
725
|
+
</div>
|
|
726
|
+
));
|
|
727
|
+
|
|
728
|
+
render(
|
|
729
|
+
<TestWrapper>
|
|
730
|
+
<CopilotChatView
|
|
731
|
+
messages={sampleMessages}
|
|
732
|
+
suggestions={suggestions}
|
|
733
|
+
suggestionView={{
|
|
734
|
+
container: CustomContainer,
|
|
735
|
+
}}
|
|
736
|
+
/>
|
|
737
|
+
</TestWrapper>,
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const container = screen.queryByTestId("custom-suggestion-container");
|
|
741
|
+
if (container) {
|
|
742
|
+
expect(container.textContent).toContain("Suggestions:");
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe("multiple nested overrides simultaneously", () => {
|
|
748
|
+
it("should allow overriding multiple nested slots at once", () => {
|
|
749
|
+
const CustomAssistant: React.FC<any> = ({ message }) => (
|
|
750
|
+
<div data-testid="nested-assistant">A: {message?.content}</div>
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
const CustomUser: React.FC<any> = ({ message }) => (
|
|
754
|
+
<div data-testid="nested-user">U: {message?.content}</div>
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
const CustomTextArea: React.FC<any> = React.forwardRef<any, any>(
|
|
758
|
+
(props, ref) => (
|
|
759
|
+
<textarea ref={ref} data-testid="nested-textarea" {...props} />
|
|
760
|
+
),
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
render(
|
|
764
|
+
<TestWrapper>
|
|
765
|
+
<CopilotChatView
|
|
766
|
+
messages={sampleMessages}
|
|
767
|
+
messageView={{
|
|
768
|
+
assistantMessage: CustomAssistant,
|
|
769
|
+
userMessage: CustomUser,
|
|
770
|
+
}}
|
|
771
|
+
input={{
|
|
772
|
+
textArea: CustomTextArea,
|
|
773
|
+
}}
|
|
774
|
+
/>
|
|
775
|
+
</TestWrapper>,
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
expect(screen.getByTestId("nested-assistant")).toBeDefined();
|
|
779
|
+
expect(screen.getByTestId("nested-user")).toBeDefined();
|
|
780
|
+
const textarea = screen.queryByTestId("nested-textarea");
|
|
781
|
+
expect(textarea).toBeDefined();
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe("three-level deep nesting", () => {
|
|
786
|
+
it("should support messageView -> assistantMessage -> toolbar drill-down", () => {
|
|
787
|
+
const CustomToolbar: React.FC<any> = ({ children }) => (
|
|
788
|
+
<div data-testid="custom-toolbar" className="custom-toolbar">
|
|
789
|
+
Custom Toolbar: {children}
|
|
790
|
+
</div>
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
render(
|
|
794
|
+
<TestWrapper>
|
|
795
|
+
<CopilotChatView
|
|
796
|
+
messages={sampleMessages}
|
|
797
|
+
messageView={{
|
|
798
|
+
assistantMessage: {
|
|
799
|
+
toolbar: CustomToolbar,
|
|
800
|
+
},
|
|
801
|
+
}}
|
|
802
|
+
/>
|
|
803
|
+
</TestWrapper>,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const toolbar = screen.queryByTestId("custom-toolbar");
|
|
807
|
+
if (toolbar) {
|
|
808
|
+
expect(toolbar.textContent).toContain("Custom Toolbar");
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("should support messageView -> assistantMessage -> copyButton drill-down", () => {
|
|
813
|
+
const CustomCopyButton: React.FC<any> = ({ onClick }) => (
|
|
814
|
+
<button data-testid="custom-copy-btn" onClick={onClick}>
|
|
815
|
+
📋 Copy
|
|
816
|
+
</button>
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
render(
|
|
820
|
+
<TestWrapper>
|
|
821
|
+
<CopilotChatView
|
|
822
|
+
messages={sampleMessages}
|
|
823
|
+
messageView={{
|
|
824
|
+
assistantMessage: {
|
|
825
|
+
copyButton: CustomCopyButton,
|
|
826
|
+
},
|
|
827
|
+
}}
|
|
828
|
+
/>
|
|
829
|
+
</TestWrapper>,
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
const copyBtn = screen.queryByTestId("custom-copy-btn");
|
|
833
|
+
if (copyBtn) {
|
|
834
|
+
expect(copyBtn.textContent).toContain("Copy");
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("should support messageView -> userMessage -> editButton drill-down", () => {
|
|
839
|
+
const CustomEditButton: React.FC<any> = ({ onClick }) => (
|
|
840
|
+
<button data-testid="custom-edit-btn" onClick={onClick}>
|
|
841
|
+
✏️ Edit
|
|
842
|
+
</button>
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
render(
|
|
846
|
+
<TestWrapper>
|
|
847
|
+
<CopilotChatView
|
|
848
|
+
messages={sampleMessages}
|
|
849
|
+
messageView={{
|
|
850
|
+
userMessage: {
|
|
851
|
+
editButton: CustomEditButton,
|
|
852
|
+
},
|
|
853
|
+
}}
|
|
854
|
+
/>
|
|
855
|
+
</TestWrapper>,
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const editBtn = screen.queryByTestId("custom-edit-btn");
|
|
859
|
+
if (editBtn) {
|
|
860
|
+
expect(editBtn.textContent).toContain("Edit");
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// ============================================================================
|
|
867
|
+
// 5. CLASSNAME OVERRIDE TESTS
|
|
868
|
+
// ============================================================================
|
|
869
|
+
describe("5. className Override with Tailwind", () => {
|
|
870
|
+
describe("className prop override", () => {
|
|
871
|
+
it("should allow className prop in object slot to override defaults", () => {
|
|
872
|
+
const { container } = render(
|
|
873
|
+
<TestWrapper>
|
|
874
|
+
<CopilotChatView
|
|
875
|
+
messages={sampleMessages}
|
|
876
|
+
messageView={{ className: "my-custom-class" }}
|
|
877
|
+
/>
|
|
878
|
+
</TestWrapper>,
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
const customElement = container.querySelector(".my-custom-class");
|
|
882
|
+
expect(customElement).toBeDefined();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("should merge className with other props in object slot", () => {
|
|
886
|
+
render(
|
|
887
|
+
<TestWrapper>
|
|
888
|
+
<CopilotChatView
|
|
889
|
+
messages={sampleMessages}
|
|
890
|
+
input={
|
|
891
|
+
{
|
|
892
|
+
className: "custom-input-class",
|
|
893
|
+
"data-testid": "input-with-class",
|
|
894
|
+
} as any
|
|
895
|
+
}
|
|
896
|
+
/>
|
|
897
|
+
</TestWrapper>,
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
const input = screen.queryByTestId("input-with-class");
|
|
901
|
+
if (input) {
|
|
902
|
+
expect(input.classList.contains("custom-input-class")).toBe(true);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
describe("string slot vs className prop equivalence", () => {
|
|
908
|
+
it("string slot should behave same as className prop", () => {
|
|
909
|
+
// String slot version
|
|
910
|
+
const { container: container1 } = render(
|
|
911
|
+
<TestWrapper>
|
|
912
|
+
<CopilotChatView
|
|
913
|
+
messages={sampleMessages}
|
|
914
|
+
input="test-class-string"
|
|
915
|
+
/>
|
|
916
|
+
</TestWrapper>,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
// className prop version
|
|
920
|
+
const { container: container2 } = render(
|
|
921
|
+
<TestWrapper>
|
|
922
|
+
<CopilotChatView
|
|
923
|
+
messages={sampleMessages}
|
|
924
|
+
input={{ className: "test-class-object" }}
|
|
925
|
+
/>
|
|
926
|
+
</TestWrapper>,
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
expect(container1.querySelector(".test-class-string")).toBeDefined();
|
|
930
|
+
expect(container2.querySelector(".test-class-object")).toBeDefined();
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
describe("tailwind utility class merging", () => {
|
|
935
|
+
it("should properly apply tailwind utilities like flex, grid, etc.", () => {
|
|
936
|
+
const { container } = render(
|
|
937
|
+
<TestWrapper>
|
|
938
|
+
<CopilotChatView
|
|
939
|
+
messages={sampleMessages}
|
|
940
|
+
input="flex items-center justify-between gap-4"
|
|
941
|
+
/>
|
|
942
|
+
</TestWrapper>,
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
// Find the input specifically (it has justify-between which is unique to our slot)
|
|
946
|
+
const flexContainer = container.querySelector(".justify-between");
|
|
947
|
+
if (flexContainer) {
|
|
948
|
+
expect(flexContainer.classList.contains("flex")).toBe(true);
|
|
949
|
+
expect(flexContainer.classList.contains("items-center")).toBe(true);
|
|
950
|
+
expect(flexContainer.classList.contains("gap-4")).toBe(true);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it("should apply responsive tailwind classes", () => {
|
|
955
|
+
const { container } = render(
|
|
956
|
+
<TestWrapper>
|
|
957
|
+
<CopilotChatView
|
|
958
|
+
messages={sampleMessages}
|
|
959
|
+
messageView="p-2 md:p-4 lg:p-6"
|
|
960
|
+
/>
|
|
961
|
+
</TestWrapper>,
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
const element = container.querySelector(".p-2");
|
|
965
|
+
if (element) {
|
|
966
|
+
expect(element.classList.contains("md:p-4")).toBe(true);
|
|
967
|
+
expect(element.classList.contains("lg:p-6")).toBe(true);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it("should apply dark mode tailwind classes", () => {
|
|
972
|
+
const { container } = render(
|
|
973
|
+
<TestWrapper>
|
|
974
|
+
<CopilotChatView
|
|
975
|
+
messages={sampleMessages}
|
|
976
|
+
scrollView="bg-white dark:bg-gray-900"
|
|
977
|
+
/>
|
|
978
|
+
</TestWrapper>,
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const element = container.querySelector(".bg-white");
|
|
982
|
+
if (element) {
|
|
983
|
+
expect(element.classList.contains("dark:bg-gray-900")).toBe(true);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
describe("user className should override pre-set className", () => {
|
|
989
|
+
it("object slot className should take precedence over defaults", () => {
|
|
990
|
+
// This tests that when user provides className in object slot,
|
|
991
|
+
// it should override/replace the default className
|
|
992
|
+
const { container } = render(
|
|
993
|
+
<TestWrapper>
|
|
994
|
+
<CopilotChatView
|
|
995
|
+
messages={sampleMessages}
|
|
996
|
+
input={{ className: "user-override-class" }}
|
|
997
|
+
/>
|
|
998
|
+
</TestWrapper>,
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const element = container.querySelector(".user-override-class");
|
|
1002
|
+
expect(element).toBeDefined();
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// ============================================================================
|
|
1008
|
+
// 6. CHILDREN RENDER FUNCTION TESTS
|
|
1009
|
+
// ============================================================================
|
|
1010
|
+
describe("6. Children Render Function (Composition Pattern)", () => {
|
|
1011
|
+
it("should support children render function for full control", () => {
|
|
1012
|
+
render(
|
|
1013
|
+
<TestWrapper>
|
|
1014
|
+
<CopilotChatView messages={sampleMessages}>
|
|
1015
|
+
{({ messageView, input }) => (
|
|
1016
|
+
<div data-testid="custom-layout">
|
|
1017
|
+
<div className="messages-area">{messageView}</div>
|
|
1018
|
+
<div className="input-area">{input}</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
)}
|
|
1021
|
+
</CopilotChatView>
|
|
1022
|
+
</TestWrapper>,
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
expect(screen.getByTestId("custom-layout")).toBeDefined();
|
|
1026
|
+
expect(document.querySelector(".messages-area")).toBeDefined();
|
|
1027
|
+
expect(document.querySelector(".input-area")).toBeDefined();
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("children render function should receive all slot elements", () => {
|
|
1031
|
+
const receivedSlots: string[] = [];
|
|
1032
|
+
|
|
1033
|
+
render(
|
|
1034
|
+
<TestWrapper>
|
|
1035
|
+
<CopilotChatView messages={sampleMessages}>
|
|
1036
|
+
{(slots) => {
|
|
1037
|
+
receivedSlots.push(...Object.keys(slots));
|
|
1038
|
+
return <div data-testid="render-check">Rendered</div>;
|
|
1039
|
+
}}
|
|
1040
|
+
</CopilotChatView>
|
|
1041
|
+
</TestWrapper>,
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
// Should receive at least messageView, input, scrollView, etc.
|
|
1045
|
+
expect(
|
|
1046
|
+
receivedSlots.includes("messageView") || receivedSlots.length > 0,
|
|
1047
|
+
).toBe(true);
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
});
|