import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { CopilotChatAssistantMessage } from "../CopilotChatAssistantMessage"; import { CopilotKitProvider } from "../../../providers/CopilotKitProvider"; import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider"; import { AssistantMessage } from "@ag-ui/core"; // Wrapper to provide required context const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); const createAssistantMessage = (content: string): AssistantMessage => ({ id: "msg-1", role: "assistant", content, }); describe("CopilotChatAssistantMessage Slot System E2E Tests", () => { // ============================================================================ // 1. TAILWIND CLASS TESTS // ============================================================================ describe("1. Tailwind Class Slot Override", () => { describe("markdownRenderer slot", () => { it("should apply tailwind class string to markdownRenderer", () => { const message = createAssistantMessage("Hello world"); const { container } = render( , ); const markdown = container.querySelector(".bg-blue-100"); expect(markdown).toBeDefined(); expect(markdown?.classList.contains("rounded-lg")).toBe(true); }); }); describe("toolbar slot", () => { it("should apply tailwind class string to toolbar", () => { const message = createAssistantMessage("Hello world"); const { container } = render( , ); const toolbar = container.querySelector(".bg-gray-100"); expect(toolbar).toBeDefined(); expect(toolbar?.classList.contains("border-t")).toBe(true); }); }); describe("copyButton slot", () => { it("should apply tailwind class string to copyButton", () => { const message = createAssistantMessage("Hello world"); const { container } = render( , ); const copyBtn = container.querySelector(".text-green-500"); expect(copyBtn).toBeDefined(); }); }); describe("thumbsUpButton slot", () => { it("should apply tailwind class string to thumbsUpButton", () => { const onThumbsUp = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const thumbsUp = container.querySelector(".text-blue-500"); expect(thumbsUp).toBeDefined(); }); }); describe("thumbsDownButton slot", () => { it("should apply tailwind class string to thumbsDownButton", () => { const onThumbsDown = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const thumbsDown = container.querySelector(".text-red-500"); expect(thumbsDown).toBeDefined(); }); }); describe("readAloudButton slot", () => { it("should apply tailwind class string to readAloudButton", () => { const onReadAloud = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const readAloud = container.querySelector(".text-purple-500"); expect(readAloud).toBeDefined(); }); }); describe("regenerateButton slot", () => { it("should apply tailwind class string to regenerateButton", () => { const onRegenerate = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const regenerate = container.querySelector(".text-orange-500"); expect(regenerate).toBeDefined(); }); }); describe("toolCallsView slot", () => { it("should apply tailwind class string to toolCallsView", () => { const message: AssistantMessage = { ...createAssistantMessage("Hello"), toolCalls: [ { id: "tc-1", type: "function", function: { name: "test_tool", arguments: "{}" }, }, ], }; const { container } = render( , ); const toolCalls = container.querySelector(".bg-yellow-50"); // May not be visible if no tool calls rendered if (toolCalls) { expect(toolCalls.classList.contains("p-2")).toBe(true); } }); }); }); // ============================================================================ // 2. PROPERTY PASSING TESTS // ============================================================================ describe("2. Property Passing (onClick, disabled, etc.)", () => { describe("markdownRenderer slot", () => { it("should pass custom props to markdownRenderer", () => { const message = createAssistantMessage("Hello world"); const { container } = render( , ); const markdown = screen.queryByTestId("custom-markdown"); expect(markdown).toBeDefined(); }); }); describe("toolbar slot", () => { it("should pass custom onClick to toolbar", () => { const onClick = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const toolbar = screen.queryByTestId("custom-toolbar"); if (toolbar) { fireEvent.click(toolbar); expect(onClick).toHaveBeenCalled(); } }); }); describe("copyButton slot", () => { it("should pass custom onClick that wraps default behavior", () => { const customOnClick = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); // Find copy button by aria-label const copyBtn = container.querySelector('button[aria-label*="Copy"]'); if (copyBtn) { fireEvent.click(copyBtn); expect(customOnClick).toHaveBeenCalled(); } }); it("should support disabled state on copyButton", () => { const message = createAssistantMessage("Hello world"); const { container } = render( , ); const copyBtn = container.querySelector('button[aria-label*="Copy"]'); if (copyBtn) { expect(copyBtn.hasAttribute("disabled")).toBe(true); } }); }); describe("thumbsUpButton slot", () => { it("should call custom onClick on thumbsUpButton", () => { const customOnClick = vi.fn(); const onThumbsUp = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const thumbsUpBtn = container.querySelector( 'button[aria-label*="Thumbs up"]', ); if (thumbsUpBtn) { fireEvent.click(thumbsUpBtn); expect(customOnClick).toHaveBeenCalled(); } }); }); describe("thumbsDownButton slot", () => { it("should call custom onClick on thumbsDownButton", () => { const customOnClick = vi.fn(); const onThumbsDown = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const thumbsDownBtn = container.querySelector( 'button[aria-label*="Thumbs down"]', ); if (thumbsDownBtn) { fireEvent.click(thumbsDownBtn); expect(customOnClick).toHaveBeenCalled(); } }); }); describe("readAloudButton slot", () => { it("should call custom onClick on readAloudButton", () => { const customOnClick = vi.fn(); const onReadAloud = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const readAloudBtn = container.querySelector( 'button[aria-label*="Read"]', ); if (readAloudBtn) { fireEvent.click(readAloudBtn); expect(customOnClick).toHaveBeenCalled(); } }); }); describe("regenerateButton slot", () => { it("should call custom onClick on regenerateButton", () => { const customOnClick = vi.fn(); const onRegenerate = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); const regenerateBtn = container.querySelector( 'button[aria-label*="Regenerate"]', ); if (regenerateBtn) { fireEvent.click(regenerateBtn); expect(customOnClick).toHaveBeenCalled(); } }); }); }); // ============================================================================ // 3. CUSTOM COMPONENT TESTS // ============================================================================ describe("3. Custom Component Receiving Sub-components", () => { it("should allow custom component for markdownRenderer", () => { const CustomMarkdown: React.FC<{ content: string }> = ({ content }) => (
{content.toUpperCase()}
); const message = createAssistantMessage("hello"); render( , ); const custom = screen.queryByTestId("custom-markdown-component"); expect(custom).toBeDefined(); expect(custom?.textContent).toBe("HELLO"); }); it("should allow custom component for toolbar", () => { const CustomToolbar: React.FC = ({ children, }) => (
Custom Toolbar: {children}
); const message = createAssistantMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-toolbar-component"); expect(custom).toBeDefined(); expect(custom?.textContent).toContain("Custom Toolbar"); }); it("should allow custom component for copyButton", () => { const CustomCopyButton: React.FC< React.ButtonHTMLAttributes > = (props) => ( ); const message = createAssistantMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-copy"); expect(custom).toBeDefined(); }); }); // ============================================================================ // 4. CHILDREN RENDER FUNCTION (DRILL-DOWN) TESTS // ============================================================================ describe("4. Children Render Function for Drill-down", () => { it("should provide all bound sub-components via children render function", () => { const message = createAssistantMessage("Hello world"); const childrenFn = vi.fn((props) => (
{props.markdownRenderer}
{props.toolbar}
{props.copyButton}
{props.thumbsUpButton}
{props.thumbsDownButton}
{props.readAloudButton}
{props.regenerateButton}
{props.toolCallsView}
)); render( {childrenFn} , ); expect(childrenFn).toHaveBeenCalled(); const callArgs = childrenFn.mock.calls[0][0]; expect(callArgs).toHaveProperty("markdownRenderer"); expect(callArgs).toHaveProperty("toolbar"); expect(callArgs).toHaveProperty("copyButton"); expect(callArgs).toHaveProperty("thumbsUpButton"); expect(callArgs).toHaveProperty("thumbsDownButton"); expect(callArgs).toHaveProperty("readAloudButton"); expect(callArgs).toHaveProperty("regenerateButton"); expect(callArgs).toHaveProperty("toolCallsView"); expect(callArgs).toHaveProperty("message"); expect(screen.queryByTestId("children-render")).toBeDefined(); }); it("should pass message and other props through children render function", () => { const message = createAssistantMessage("Test message"); const childrenFn = vi.fn(() =>
); render( {childrenFn} , ); const callArgs = childrenFn.mock.calls[0][0]; expect(callArgs.message).toBe(message); expect(callArgs.isRunning).toBe(true); expect(callArgs.toolbarVisible).toBe(false); }); }); // ============================================================================ // 5. CLASSNAME OVERRIDE TESTS // ============================================================================ describe("5. className Override with Tailwind Strings", () => { it("should override root className while preserving default prose classes", () => { const message = createAssistantMessage("Hello"); const { container } = render( , ); const root = container.querySelector(".custom-root-class"); expect(root).toBeDefined(); // Prose classes are on an inner div wrapping the markdown content const proseDiv = root?.querySelector(".cpk\\:prose"); expect(proseDiv).toBeDefined(); }); it("should allow tailwind utilities to override default styles", () => { const message = createAssistantMessage("Hello"); const { container } = render( , ); // max-w-sm should override the default max-w-full const root = container.querySelector(".max-w-sm"); expect(root).toBeDefined(); }); it("should merge multiple slot classNames correctly", () => { const message = createAssistantMessage("Hello"); const { container } = render( , ); expect(container.querySelector(".root-custom")).toBeDefined(); expect(container.querySelector(".toolbar-custom")).toBeDefined(); expect(container.querySelector(".copy-custom")).toBeDefined(); }); }); // ============================================================================ // 6. INTEGRATION / RECURSIVE SLOT TESTS // ============================================================================ describe("6. Integration and Recursive Slot Application", () => { it("should correctly render all slots with mixed customization", () => { const onThumbsUp = vi.fn(); const onThumbsDown = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); expect(container.querySelector(".markdown-style")).toBeDefined(); expect(container.querySelector(".toolbar-style")).toBeDefined(); expect(container.querySelector(".copy-style")).toBeDefined(); expect(container.querySelector(".thumbs-up-style")).toBeDefined(); expect(container.querySelector(".thumbs-down-style")).toBeDefined(); }); it("should work with property objects and class strings mixed", () => { const onClick = vi.fn(); const message = createAssistantMessage("Hello world"); const { container } = render( , ); expect(container.querySelector(".text-lg")).toBeDefined(); const toolbar = container.querySelector(".flex.gap-2"); if (toolbar) { fireEvent.click(toolbar); expect(onClick).toHaveBeenCalled(); } }); }); });