/**
* @vitest-environment jsdom
*/
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { CopilotChatView } from "../CopilotChatView";
import { CopilotChatInput } from "../CopilotChatInput";
import { CopilotChatMessageView } from "../CopilotChatMessageView";
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
// Wrapper to provide required context
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
);
const createMessages = () => [
{ id: "1", role: "user" as const, content: "Hello" },
{ id: "2", role: "assistant" as const, content: "Hi there! How can I help?" },
{ id: "3", role: "user" as const, content: "Tell me a joke" },
{
id: "4",
role: "assistant" as const,
content: "Why did the chicken cross the road?",
},
];
const createSuggestions = () => [
{
title: "Tell me more",
message: "Tell me more about that",
isLoading: false,
},
{
title: "Another topic",
message: "Let's talk about something else",
isLoading: false,
},
];
describe("CopilotChatView onClick Handlers - Drill-Down E2E Tests", () => {
// ============================================================================
// LEVEL 1: CopilotChatView Direct Slots
// ============================================================================
describe("Level 1: CopilotChatView Direct Slots", () => {
describe("scrollToBottomButton onClick (nested under scrollView)", () => {
it("should handle onClick on scrollToBottomButton via scrollView props", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// Find and click the scroll to bottom button (may need scrolling to appear)
const scrollBtn =
container.querySelector('[aria-label*="scroll"]') ||
container.querySelector('button[class*="scroll"]');
if (scrollBtn) {
fireEvent.click(scrollBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("input onClick", () => {
it("should handle onClick on input via props object", () => {
const onClick = vi.fn();
const { container } = render(
,
);
const input =
screen.queryByTestId("input-slot") ||
container.querySelector('[data-slot="input"]');
if (input) {
fireEvent.click(input);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("suggestionView onClick", () => {
it("should handle onSelectSuggestion when suggestion is clicked", () => {
const onSelectSuggestion = vi.fn();
render(
,
);
const suggestion = screen.queryByText("Tell me more");
if (suggestion) {
fireEvent.click(suggestion);
expect(onSelectSuggestion).toHaveBeenCalled();
}
});
});
});
// ============================================================================
// LEVEL 2: CopilotChatInput Drill-Down
// ============================================================================
describe("Level 2: CopilotChatInput Drill-Down", () => {
describe("input -> sendButton onClick", () => {
it("should handle onClick on sendButton via input props drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// Find send button by aria-label or common patterns
const sendBtn =
container.querySelector('button[aria-label*="Send"]') ||
container.querySelector('button[type="submit"]');
if (sendBtn) {
fireEvent.click(sendBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("input -> startTranscribeButton onClick", () => {
it("should handle onClick on startTranscribeButton via input props drill-down", () => {
const onClick = vi.fn();
const onStartTranscribe = vi.fn();
const { container } = render(
,
);
const transcribeBtn =
container.querySelector('button[aria-label*="transcribe"]') ||
container.querySelector('button[aria-label*="voice"]') ||
container.querySelector('button[aria-label*="microphone"]');
if (transcribeBtn) {
fireEvent.click(transcribeBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("input -> addMenuButton onClick", () => {
it("should handle onClick on addMenuButton via input props drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
const addBtn =
container.querySelector('button[aria-label*="add"]') ||
container.querySelector('button[aria-label*="plus"]') ||
container.querySelector('button[aria-label*="menu"]');
if (addBtn) {
fireEvent.click(addBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("input -> textArea onFocus/onBlur", () => {
it("should handle onFocus on textArea via input props drill-down", () => {
const onFocus = vi.fn();
const { container } = render(
,
);
const textarea = container.querySelector("textarea");
if (textarea) {
fireEvent.focus(textarea);
expect(onFocus).toHaveBeenCalled();
}
});
it("should handle onBlur on textArea via input props drill-down", () => {
const onBlur = vi.fn();
const { container } = render(
,
);
const textarea = container.querySelector("textarea");
if (textarea) {
fireEvent.focus(textarea);
fireEvent.blur(textarea);
expect(onBlur).toHaveBeenCalled();
}
});
});
});
// ============================================================================
// LEVEL 2: CopilotChatMessageView Drill-Down
// ============================================================================
describe("Level 2: CopilotChatMessageView Drill-Down", () => {
describe("messageView -> assistantMessage onClick", () => {
it("should handle onClick on assistantMessage container via messageView drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// Find assistant message by data-message-id or content
const assistantMsg =
container.querySelector('[data-message-id="2"]') ||
container.querySelector(".prose");
if (assistantMsg) {
fireEvent.click(assistantMsg);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("messageView -> userMessage onClick", () => {
it("should handle onClick on userMessage container via messageView drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
const userMsg = container.querySelector('[data-message-id="1"]');
if (userMsg) {
fireEvent.click(userMsg);
expect(onClick).toHaveBeenCalled();
}
});
});
});
// ============================================================================
// LEVEL 3: CopilotChatAssistantMessage Toolbar Drill-Down
// ============================================================================
describe("Level 3: CopilotChatAssistantMessage Toolbar Drill-Down", () => {
describe("messageView -> assistantMessage -> copyButton onClick", () => {
it("should handle onClick on copyButton via deep drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// Find copy button in assistant message toolbar (message id "2" is an assistant message)
const assistantMsg = container.querySelector('[data-message-id="2"]');
const copyBtn = assistantMsg?.querySelector(
'button[aria-label*="Copy"]',
);
if (copyBtn) {
fireEvent.click(copyBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("messageView -> assistantMessage -> thumbsUpButton onClick", () => {
it("should handle onClick on thumbsUpButton via deep drill-down", () => {
const onClick = vi.fn();
const onThumbsUp = vi.fn();
const { container } = render(
,
);
const thumbsUpBtn = container.querySelector(
'button[aria-label*="Thumbs up"]',
);
if (thumbsUpBtn) {
fireEvent.click(thumbsUpBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("messageView -> assistantMessage -> thumbsDownButton onClick", () => {
it("should handle onClick on thumbsDownButton via deep drill-down", () => {
const onClick = vi.fn();
const onThumbsDown = vi.fn();
const { container } = render(
,
);
const thumbsDownBtn = container.querySelector(
'button[aria-label*="Thumbs down"]',
);
if (thumbsDownBtn) {
fireEvent.click(thumbsDownBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("messageView -> assistantMessage -> readAloudButton onClick", () => {
it("should handle onClick on readAloudButton via deep drill-down", () => {
const onClick = vi.fn();
const onReadAloud = vi.fn();
const { container } = render(
,
);
const readAloudBtn = container.querySelector(
'button[aria-label*="Read"]',
);
if (readAloudBtn) {
fireEvent.click(readAloudBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("messageView -> assistantMessage -> regenerateButton onClick", () => {
it("should handle onClick on regenerateButton via deep drill-down", () => {
const onClick = vi.fn();
const onRegenerate = vi.fn();
const { container } = render(
,
);
const regenerateBtn = container.querySelector(
'button[aria-label*="Regenerate"]',
);
if (regenerateBtn) {
fireEvent.click(regenerateBtn);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("messageView -> assistantMessage -> toolbar onClick", () => {
it("should handle onClick on entire toolbar via deep drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// Find toolbar container (usually contains the buttons)
const toolbars = container.querySelectorAll('[class*="toolbar"]');
const firstToolbar = toolbars[0];
if (firstToolbar) {
fireEvent.click(firstToolbar);
expect(onClick).toHaveBeenCalled();
}
});
});
});
// ============================================================================
// LEVEL 3: CopilotChatUserMessage Toolbar Drill-Down
// ============================================================================
describe("Level 3: CopilotChatUserMessage Toolbar Drill-Down", () => {
describe("messageView -> userMessage -> copyButton onClick", () => {
it("should handle onClick on copyButton via deep drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// User message copy buttons may be in hover state
// Need to trigger hover first or find them directly
const userMsgContainers = container.querySelectorAll(
'[data-message-id="1"], [data-message-id="3"]',
);
const firstUserMsg = userMsgContainers[0];
if (firstUserMsg) {
// Trigger mouseenter to show toolbar
fireEvent.mouseEnter(firstUserMsg);
const copyBtn = container.querySelector('button[aria-label*="Copy"]');
if (copyBtn) {
fireEvent.click(copyBtn);
expect(onClick).toHaveBeenCalled();
}
}
});
});
describe("messageView -> userMessage -> editButton onClick", () => {
it("should handle onClick on editButton via deep drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
const userMsgContainers = container.querySelectorAll(
'[data-message-id="1"], [data-message-id="3"]',
);
const firstUserMsg = userMsgContainers[0];
if (firstUserMsg) {
fireEvent.mouseEnter(firstUserMsg);
const editBtn = container.querySelector('button[aria-label*="Edit"]');
if (editBtn) {
fireEvent.click(editBtn);
expect(onClick).toHaveBeenCalled();
}
}
});
});
});
// ============================================================================
// LEVEL 2: SuggestionView Drill-Down
// ============================================================================
describe("Level 2: SuggestionView Drill-Down", () => {
describe("suggestionView -> container onClick", () => {
it("should handle onClick on suggestion container via drill-down", () => {
const onClick = vi.fn();
const { container } = render(
,
);
// Find suggestion container
const suggestionContainer = container.querySelector(
'[data-testid="suggestion-container"]',
);
if (suggestionContainer) {
fireEvent.click(suggestionContainer);
expect(onClick).toHaveBeenCalled();
}
});
});
describe("suggestionView -> suggestion onClick", () => {
it("should handle onClick on individual suggestion pills via drill-down", () => {
const onClick = vi.fn();
render(
,
);
const suggestionPill = screen.queryByText("Tell me more");
if (suggestionPill) {
fireEvent.click(suggestionPill);
expect(onClick).toHaveBeenCalled();
}
});
});
});
// ============================================================================
// FUNCTION RENDER SLOT PATTERN TESTS
// ============================================================================
describe("Function Render Slot Pattern", () => {
describe("input slot with render function", () => {
it("should support passing render function to input slot", () => {
const onSubmitMessage = vi.fn();
const CustomInput = (props: any) => (
);
render(
,
);
// The custom send class should be applied
const sendBtn = document.querySelector(".custom-send-class");
expect(sendBtn).toBeDefined();
});
});
describe("messageView slot with render function", () => {
it("should support passing render function to messageView slot", () => {
const CustomMessageView = (props: any) => (
);
render(
,
);
const messageView = document.querySelector(".custom-message-view");
expect(messageView).toBeDefined();
});
});
});
// ============================================================================
// CALLBACK PROPAGATION TESTS
// ============================================================================
describe("Callback Propagation Through Slot Hierarchy", () => {
describe("onSubmitMessage propagation", () => {
it("should propagate onSubmitMessage through input slot", () => {
const onSubmitMessage = vi.fn();
const { container } = render(
,
);
const textarea = container.querySelector("textarea");
const form = container.querySelector("form");
if (textarea && form) {
fireEvent.change(textarea, { target: { value: "Test message" } });
fireEvent.submit(form);
expect(onSubmitMessage).toHaveBeenCalledWith("Test message");
}
});
});
describe("onStop propagation", () => {
it("should propagate onStop through input slot", () => {
const onStop = vi.fn();
const { container } = render(
,
);
// Find stop button (usually appears when isRunning=true)
const stopBtn =
container.querySelector('button[aria-label*="Stop"]') ||
container.querySelector('button[aria-label*="stop"]');
if (stopBtn) {
fireEvent.click(stopBtn);
expect(onStop).toHaveBeenCalled();
}
});
});
describe("onThumbsUp/onThumbsDown propagation", () => {
it("should propagate onThumbsUp through messageView slot", () => {
const onThumbsUp = vi.fn();
const { container } = render(
,
);
const thumbsUpBtn = container.querySelector(
'button[aria-label*="Thumbs up"]',
);
if (thumbsUpBtn) {
fireEvent.click(thumbsUpBtn);
expect(onThumbsUp).toHaveBeenCalled();
}
});
it("should propagate onThumbsDown through messageView slot", () => {
const onThumbsDown = vi.fn();
const { container } = render(
,
);
const thumbsDownBtn = container.querySelector(
'button[aria-label*="Thumbs down"]',
);
if (thumbsDownBtn) {
fireEvent.click(thumbsDownBtn);
expect(onThumbsDown).toHaveBeenCalled();
}
});
});
describe("onEditMessage propagation", () => {
it("should propagate onEditMessage through messageView slot", () => {
const onEditMessage = vi.fn();
const { container } = render(
,
);
// Find user message and hover to show toolbar
const userMsgContainers = container.querySelectorAll(
'[data-message-id="1"]',
);
const firstUserMsg = userMsgContainers[0];
if (firstUserMsg) {
fireEvent.mouseEnter(firstUserMsg);
const editBtn = container.querySelector('button[aria-label*="Edit"]');
if (editBtn) {
fireEvent.click(editBtn);
expect(onEditMessage).toHaveBeenCalled();
}
}
});
});
});
// ============================================================================
// COMBINED CUSTOMIZATION WITH ONCLICK
// ============================================================================
describe("Combined Customization with onClick", () => {
it("should handle onClick alongside tailwind class customization", () => {
const onClick = vi.fn();
const { container } = render(
,
);
const copyBtn =
container.querySelector(".custom-copy-class") ||
container.querySelector('button[aria-label*="Copy"]');
if (copyBtn) {
fireEvent.click(copyBtn);
expect(onClick).toHaveBeenCalled();
}
});
it("should allow custom component with onClick handling", () => {
const customOnClick = vi.fn();
const CustomCopyButton: React.FC<
React.ButtonHTMLAttributes
> = ({ onClick, ...props }) => (
);
render(
,
);
// Multiple assistant messages have custom copy buttons
const customCopyButtons = screen.queryAllByTestId("custom-copy");
if (customCopyButtons.length > 0) {
fireEvent.click(customCopyButtons[0]);
expect(customOnClick).toHaveBeenCalled();
}
});
});
});