import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { vi } from "vitest";
import { CopilotChatAssistantMessage } from "../CopilotChatAssistantMessage";
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
import { AssistantMessage } from "@ag-ui/core";
// No mocks needed - Vitest handles ES modules natively!
// Mock navigator.clipboard
const mockWriteText = vi.fn();
Object.assign(navigator, {
clipboard: {
writeText: mockWriteText,
},
});
// Mock callback functions
const mockOnThumbsUp = vi.fn();
const mockOnThumbsDown = vi.fn();
const mockOnReadAloud = vi.fn();
const mockOnRegenerate = vi.fn();
// Helper to render components with context providers
const TEST_THREAD_ID = "test-thread";
const renderWithProvider = (component: React.ReactElement) => {
return render(
{component}
,
);
};
// Clear mocks before each test
beforeEach(() => {
mockWriteText.mockClear();
mockOnThumbsUp.mockClear();
mockOnThumbsDown.mockClear();
mockOnReadAloud.mockClear();
mockOnRegenerate.mockClear();
});
describe("CopilotChatAssistantMessage", () => {
const basicMessage: AssistantMessage = {
role: "assistant",
content: "Hello, this is a test message from the assistant.",
id: "test-message-1",
};
describe("Basic rendering", () => {
it("renders with default components and styling", () => {
renderWithProvider(
,
);
// Check if elements exist (getBy throws if not found, so this is sufficient)
// Note: Since markdown may not render in test environment, let's check the component structure
const copyButton = screen.getByRole("button", { name: /copy/i });
expect(copyButton).toBeDefined();
});
it("renders empty message gracefully", () => {
const emptyMessage: AssistantMessage = {
role: "assistant",
content: "",
id: "empty-message",
};
renderWithProvider(
,
);
// Should render the component structure but NOT show toolbar for empty content
const container = document.querySelector(
'[data-message-id="empty-message"]',
);
expect(container).toBeDefined();
// Should NOT have a copy button since there's no content to copy
expect(screen.queryByRole("button", { name: /copy/i })).toBeNull();
});
});
describe("Callback functionality", () => {
it("renders only copy button when no callbacks provided", () => {
renderWithProvider(
,
);
expect(screen.getByRole("button", { name: /copy/i })).toBeDefined();
expect(screen.queryByRole("button", { name: /thumbs up/i })).toBeNull();
expect(screen.queryByRole("button", { name: /thumbs down/i })).toBeNull();
expect(screen.queryByRole("button", { name: /read aloud/i })).toBeNull();
expect(screen.queryByRole("button", { name: /regenerate/i })).toBeNull();
});
it("renders all buttons when all callbacks provided", () => {
renderWithProvider(
,
);
expect(screen.getByRole("button", { name: /copy/i })).toBeDefined();
expect(
screen.getByRole("button", { name: /good response/i }),
).toBeDefined();
expect(
screen.getByRole("button", { name: /bad response/i }),
).toBeDefined();
expect(screen.getByRole("button", { name: /read aloud/i })).toBeDefined();
expect(screen.getByRole("button", { name: /regenerate/i })).toBeDefined();
});
it("calls copy functionality when copy button clicked", async () => {
renderWithProvider(
,
);
const copyButton = screen.getByRole("button", { name: /copy/i });
fireEvent.click(copyButton);
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith(basicMessage.content!);
});
});
it("calls thumbs up callback when thumbs up button clicked", () => {
renderWithProvider(
,
);
const thumbsUpButton = screen.getByRole("button", {
name: /good response/i,
});
fireEvent.click(thumbsUpButton);
expect(mockOnThumbsUp).toHaveBeenCalledTimes(1);
});
it("calls thumbs down callback when thumbs down button clicked", () => {
renderWithProvider(
,
);
const thumbsDownButton = screen.getByRole("button", {
name: /bad response/i,
});
fireEvent.click(thumbsDownButton);
expect(mockOnThumbsDown).toHaveBeenCalledTimes(1);
});
it("calls read aloud callback when read aloud button clicked", () => {
renderWithProvider(
,
);
const readAloudButton = screen.getByRole("button", {
name: /read aloud/i,
});
fireEvent.click(readAloudButton);
expect(mockOnReadAloud).toHaveBeenCalledTimes(1);
});
it("calls regenerate callback when regenerate button clicked", () => {
renderWithProvider(
,
);
const regenerateButton = screen.getByRole("button", {
name: /regenerate/i,
});
fireEvent.click(regenerateButton);
expect(mockOnRegenerate).toHaveBeenCalledTimes(1);
});
});
describe("Additional toolbar items", () => {
it("renders additional toolbar items", () => {
const additionalItems = (
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-toolbar-item")).toBeDefined();
});
});
describe("Slot functionality - Custom Components", () => {
it("accepts custom MarkdownRenderer component", () => {
const CustomMarkdownRenderer = ({ content }: { content: string }) => (
{content.toUpperCase()}
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-markdown")).toBeDefined();
expect(
screen
.getByTestId("custom-markdown")
.textContent?.includes(basicMessage.content!.toUpperCase()),
).toBe(true);
});
it("accepts custom Toolbar component", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomToolbar = ({ children, ...props }: any) => (
Custom Toolbar: {children}
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-toolbar")).toBeDefined();
expect(
screen
.getByTestId("custom-toolbar")
.textContent?.includes("Custom Toolbar:"),
).toBe(true);
});
it("accepts custom CopyButton component", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomCopyButton = (props: any) => (
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-copy-button")).toBeDefined();
expect(
screen
.getByTestId("custom-copy-button")
.textContent?.includes("Custom Copy"),
).toBe(true);
});
it("accepts custom ThumbsUpButton component", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomThumbsUpButton = (props: any) => (
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-thumbs-up")).toBeDefined();
expect(
screen
.getByTestId("custom-thumbs-up")
.textContent?.includes("Custom Like"),
).toBe(true);
});
it("accepts custom ThumbsDownButton component", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomThumbsDownButton = (props: any) => (
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-thumbs-down")).toBeDefined();
expect(
screen
.getByTestId("custom-thumbs-down")
.textContent?.includes("Custom Dislike"),
).toBe(true);
});
it("accepts custom ReadAloudButton component", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomReadAloudButton = (props: any) => (
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-read-aloud")).toBeDefined();
expect(
screen
.getByTestId("custom-read-aloud")
.textContent?.includes("Custom Speak"),
).toBe(true);
});
it("accepts custom RegenerateButton component", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CustomRegenerateButton = (props: any) => (
);
renderWithProvider(
,
);
expect(screen.getByTestId("custom-regenerate")).toBeDefined();
expect(
screen
.getByTestId("custom-regenerate")
.textContent?.includes("Custom Retry"),
).toBe(true);
});
});
describe("Slot functionality - Custom Classes", () => {
it("applies custom className to component", () => {
const { container } = renderWithProvider(
,
);
const containerElement = container.querySelector(
".custom-container-class",
);
expect(containerElement).toBeDefined();
});
it("applies custom className to MarkdownRenderer slot", () => {
const { container } = renderWithProvider(
,
);
const markdownElement = container.querySelector(".custom-markdown-class");
expect(markdownElement).toBeDefined();
});
it("applies custom className to Toolbar slot", () => {
const { container } = renderWithProvider(
,
);
const toolbarElement = container.querySelector(".custom-toolbar-class");
expect(toolbarElement).toBeDefined();
});
it("applies custom className to CopyButton slot", () => {
const { container } = renderWithProvider(
,
);
const copyButtonElement = container.querySelector(
".custom-copy-button-class",
);
expect(copyButtonElement).toBeDefined();
});
});
describe("Children render prop functionality", () => {
it("supports custom layout via children render prop", () => {
renderWithProvider(
{({
markdownRenderer: MarkdownRenderer,
toolbar: Toolbar,
message,
}) => (
Custom Layout for: {message.id}
{MarkdownRenderer}
{Toolbar}
)}
,
);
expect(screen.getByTestId("custom-layout")).toBeDefined();
expect(
screen.getByText(`Custom Layout for: ${basicMessage.id}`),
).toBeDefined();
expect(screen.getByTestId("custom-toolbar-wrapper")).toBeDefined();
// Note: Markdown content may not render in test environment, check toolbar instead
expect(screen.getByTestId("custom-toolbar-wrapper")).toBeDefined();
});
it("provides all slot components to children render prop", () => {
renderWithProvider(
{({
markdownRenderer: MarkdownRenderer,
toolbar: Toolbar,
copyButton: CopyButton,
thumbsUpButton: ThumbsUpButton,
thumbsDownButton: ThumbsDownButton,
readAloudButton: ReadAloudButton,
regenerateButton: RegenerateButton,
}) => (
{MarkdownRenderer}
{Toolbar}
{CopyButton}
{ThumbsUpButton}
{ThumbsDownButton}
{ReadAloudButton}
{RegenerateButton}
)}
,
);
expect(screen.getByTestId("all-slots-layout")).toBeDefined();
expect(screen.getByTestId("individual-buttons")).toBeDefined();
// Verify all buttons are rendered
const buttons = screen.getAllByRole("button");
expect(buttons.length).toBeGreaterThanOrEqual(5); // At least copy, thumbs up, thumbs down, read aloud, regenerate
});
it("provides callback props to children render prop", () => {
renderWithProvider(
{({ onThumbsUp, onThumbsDown, onReadAloud, onRegenerate }) => (
)}
,
);
fireEvent.click(screen.getByTestId("custom-thumbs-up"));
fireEvent.click(screen.getByTestId("custom-thumbs-down"));
fireEvent.click(screen.getByTestId("custom-read-aloud"));
fireEvent.click(screen.getByTestId("custom-regenerate"));
expect(mockOnThumbsUp).toHaveBeenCalledTimes(1);
expect(mockOnThumbsDown).toHaveBeenCalledTimes(1);
expect(mockOnReadAloud).toHaveBeenCalledTimes(1);
expect(mockOnRegenerate).toHaveBeenCalledTimes(1);
});
});
describe("Toolbar visibility functionality", () => {
it("shows toolbar by default (toolbarVisible = true by default)", () => {
renderWithProvider(
,
);
expect(screen.getByRole("button", { name: /copy/i })).toBeDefined();
});
it("shows toolbar when toolbarVisible is explicitly true", () => {
renderWithProvider(
,
);
expect(screen.getByRole("button", { name: /copy/i })).toBeDefined();
});
it("hides toolbar when toolbarVisible is false", () => {
renderWithProvider(
,
);
expect(screen.queryByRole("button", { name: /copy/i })).toBeNull();
});
it("always passes toolbar and toolbarVisible to children render prop", () => {
const childrenSpy = vi.fn(() => );
renderWithProvider(
{childrenSpy}
,
);
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
toolbar: expect.anything(),
toolbarVisible: false,
message: basicMessage,
}),
);
expect(screen.getByTestId("children-render")).toBeDefined();
});
it("passes toolbarVisible true to children render prop by default", () => {
const childrenSpy = vi.fn(() => );
renderWithProvider(
{childrenSpy}
,
);
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
toolbar: expect.anything(),
toolbarVisible: true,
message: basicMessage,
}),
);
});
it("children can use toolbarVisible to conditionally render toolbar", () => {
renderWithProvider(
{({ toolbar, toolbarVisible }) => (
Custom content
{toolbarVisible && (
{toolbar}
)}
{!toolbarVisible && (
No toolbar
)}
)}
,
);
expect(screen.getByTestId("custom-layout")).toBeDefined();
expect(screen.getByTestId("content")).toBeDefined();
expect(screen.queryByTestId("conditional-toolbar")).toBeNull();
expect(screen.getByTestId("no-toolbar")).toBeDefined();
});
});
describe("Error handling", () => {
it("handles copy errors gracefully", async () => {
// Mock clipboard to throw an error
mockWriteText.mockRejectedValueOnce(new Error("Clipboard error"));
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
renderWithProvider(
,
);
const copyButton = screen.getByRole("button", { name: /copy/i });
fireEvent.click(copyButton);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
"Failed to copy to clipboard:",
expect.any(Error),
);
});
consoleSpy.mockRestore();
});
it("handles null message content gracefully", () => {
const nullContentMessage: AssistantMessage = {
role: "assistant",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: null as any,
id: "null-content",
};
renderWithProvider(
,
);
// Should render the component structure but NOT show toolbar for empty content
const container = document.querySelector(
'[data-message-id="null-content"]',
);
expect(container).toBeDefined();
// Should NOT have a copy button since there's no content to copy
expect(screen.queryByRole("button", { name: /copy/i })).toBeNull();
});
});
});