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(); }); }); });