import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { CopilotChatUserMessage } from "../CopilotChatUserMessage"; import { CopilotKitProvider } from "../../../providers/CopilotKitProvider"; import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider"; import { UserMessage } from "@ag-ui/core"; // Wrapper to provide required context const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); const createUserMessage = (content: string): UserMessage => ({ id: "msg-1", role: "user", content, }); describe("CopilotChatUserMessage Slot System E2E Tests", () => { // ============================================================================ // 1. TAILWIND CLASS TESTS // ============================================================================ describe("1. Tailwind Class Slot Override", () => { describe("messageRenderer slot", () => { it("should apply tailwind class string to messageRenderer", () => { const message = createUserMessage("Hello"); const { container } = render( , ); const renderer = container.querySelector(".bg-blue-500"); expect(renderer).toBeDefined(); expect(renderer?.classList.contains("text-white")).toBe(true); expect(renderer?.classList.contains("rounded-xl")).toBe(true); }); }); describe("toolbar slot", () => { it("should apply tailwind class string to toolbar", () => { const message = createUserMessage("Hello"); const { container } = render( , ); const toolbar = container.querySelector(".bg-gray-50"); expect(toolbar).toBeDefined(); expect(toolbar?.classList.contains("border")).toBe(true); }); }); describe("copyButton slot", () => { it("should apply tailwind class string to copyButton", () => { const message = createUserMessage("Hello"); const { container } = render( , ); const copyBtn = container.querySelector(".text-indigo-500"); expect(copyBtn).toBeDefined(); }); }); describe("editButton slot", () => { it("should apply tailwind class string to editButton", () => { const onEditMessage = vi.fn(); const message = createUserMessage("Hello"); const { container } = render( , ); const editBtn = container.querySelector(".text-yellow-500"); expect(editBtn).toBeDefined(); }); }); describe("branchNavigation slot", () => { it("should apply tailwind class string to branchNavigation", () => { const onSwitchToBranch = vi.fn(); const message = createUserMessage("Hello"); const { container } = render( , ); const branchNav = container.querySelector(".bg-slate-100"); expect(branchNav).toBeDefined(); expect(branchNav?.classList.contains("px-2")).toBe(true); }); }); }); // ============================================================================ // 2. PROPERTY PASSING TESTS // ============================================================================ describe("2. Property Passing (onClick, disabled, etc.)", () => { describe("messageRenderer slot", () => { it("should pass custom props to messageRenderer", () => { const message = createUserMessage("Hello"); render( , ); const renderer = screen.queryByTestId("custom-message-renderer"); expect(renderer).toBeDefined(); }); }); describe("toolbar slot", () => { it("should pass custom onClick to toolbar", () => { const onClick = vi.fn(); const message = createUserMessage("Hello"); 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 = createUserMessage("Hello"); const { container } = render( , ); 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 = createUserMessage("Hello"); const { container } = render( , ); const copyBtn = container.querySelector('button[aria-label*="Copy"]'); if (copyBtn) { expect(copyBtn.hasAttribute("disabled")).toBe(true); } }); }); describe("editButton slot", () => { it("should call custom onClick on editButton", () => { const customOnClick = vi.fn(); const onEditMessage = vi.fn(); const message = createUserMessage("Hello"); const { container } = render( , ); const editBtn = container.querySelector('button[aria-label*="Edit"]'); if (editBtn) { fireEvent.click(editBtn); expect(customOnClick).toHaveBeenCalled(); } }); it("should support disabled state on editButton", () => { const onEditMessage = vi.fn(); const message = createUserMessage("Hello"); const { container } = render( , ); const editBtn = container.querySelector('button[aria-label*="Edit"]'); if (editBtn) { expect(editBtn.hasAttribute("disabled")).toBe(true); } }); }); describe("branchNavigation slot", () => { it("should pass custom props to branchNavigation", () => { const onSwitchToBranch = vi.fn(); const message = createUserMessage("Hello"); render( , ); const branchNav = screen.queryByTestId("custom-branch-nav"); expect(branchNav).toBeDefined(); }); }); }); // ============================================================================ // 3. CUSTOM COMPONENT TESTS // ============================================================================ describe("3. Custom Component Receiving Sub-components", () => { it("should allow custom component for messageRenderer", () => { const CustomRenderer: React.FC<{ content: string }> = ({ content }) => (
[{content}]
); const message = createUserMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-renderer"); expect(custom).toBeDefined(); expect(custom?.textContent).toBe("[Hello]"); }); it("should allow custom component for toolbar", () => { const CustomToolbar: React.FC = ({ children, }) => (
Actions: {children}
); const message = createUserMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-toolbar-component"); expect(custom).toBeDefined(); expect(custom?.textContent).toContain("Actions"); }); it("should allow custom component for copyButton", () => { const CustomCopyButton: React.FC< React.ButtonHTMLAttributes > = (props) => ( ); const message = createUserMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-copy-btn"); expect(custom).toBeDefined(); expect(custom?.textContent).toBe("Copy It"); }); it("should allow custom component for editButton", () => { const CustomEditButton: React.FC< React.ButtonHTMLAttributes > = (props) => ( ); const onEditMessage = vi.fn(); const message = createUserMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-edit-btn"); expect(custom).toBeDefined(); }); it("should allow custom component for branchNavigation", () => { const CustomBranchNav: React.FC<{ currentBranch?: number; numberOfBranches?: number; }> = ({ currentBranch = 0, numberOfBranches = 1 }) => (
Branch {currentBranch + 1} of {numberOfBranches}
); const onSwitchToBranch = vi.fn(); const message = createUserMessage("Hello"); render( , ); const custom = screen.queryByTestId("custom-branch-nav"); expect(custom).toBeDefined(); expect(custom?.textContent).toBe("Branch 2 of 3"); }); }); // ============================================================================ // 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 = createUserMessage("Hello"); const onEditMessage = vi.fn(); const onSwitchToBranch = vi.fn(); const childrenFn = vi.fn((props) => (
{props.messageRenderer}
{props.toolbar}
{props.copyButton}
{props.editButton}
{props.branchNavigation}
)); render( {childrenFn} , ); expect(childrenFn).toHaveBeenCalled(); const callArgs = childrenFn.mock.calls[0][0]; expect(callArgs).toHaveProperty("messageRenderer"); expect(callArgs).toHaveProperty("toolbar"); expect(callArgs).toHaveProperty("copyButton"); expect(callArgs).toHaveProperty("editButton"); expect(callArgs).toHaveProperty("branchNavigation"); expect(callArgs).toHaveProperty("message"); expect(screen.queryByTestId("children-render")).toBeDefined(); }); it("should pass message and branch info through children render function", () => { const message = createUserMessage("Test message"); const childrenFn = vi.fn(() =>
); render( {childrenFn} , ); const callArgs = childrenFn.mock.calls[0][0]; expect(callArgs.message).toBe(message); expect(callArgs.branchIndex).toBe(1); expect(callArgs.numberOfBranches).toBe(3); }); it("should allow reorganizing sub-components in children render", () => { const message = createUserMessage("Hello"); const onEditMessage = vi.fn(); const { container } = render( {({ messageRenderer, toolbar, copyButton, editButton }) => (
{messageRenderer}
{editButton} {copyButton}
{toolbar}
)}
, ); const customLayout = screen.queryByTestId("custom-layout"); expect(customLayout).toBeDefined(); expect(container.querySelector(".message-area")).toBeDefined(); expect(container.querySelector(".actions-row")).toBeDefined(); expect(container.querySelector(".toolbar-area")).toBeDefined(); }); }); // ============================================================================ // 5. CLASSNAME OVERRIDE TESTS // ============================================================================ describe("5. className Override with Tailwind Strings", () => { it("should override root className while preserving default layout classes", () => { const message = createUserMessage("Hello"); const { container } = render( , ); const root = container.querySelector(".custom-root-class"); expect(root).toBeDefined(); expect(root?.classList.contains("bg-purple-50")).toBe(true); }); it("should allow tailwind utilities to override default styles", () => { const message = createUserMessage("Hello"); const { container } = render( , ); // pt-0 should override the default pt-10 const root = container.querySelector(".pt-0"); expect(root).toBeDefined(); }); it("should merge multiple slot classNames correctly", () => { const onEditMessage = vi.fn(); const message = createUserMessage("Hello"); const { container } = render( , ); expect(container.querySelector(".root-custom")).toBeDefined(); expect(container.querySelector(".renderer-custom")).toBeDefined(); expect(container.querySelector(".toolbar-custom")).toBeDefined(); expect(container.querySelector(".copy-custom")).toBeDefined(); expect(container.querySelector(".edit-custom")).toBeDefined(); }); }); // ============================================================================ // 6. INTEGRATION / RECURSIVE SLOT TESTS // ============================================================================ describe("6. Integration and Recursive Slot Application", () => { it("should correctly render all slots with mixed customization", () => { const onEditMessage = vi.fn(); const onSwitchToBranch = vi.fn(); const message = createUserMessage("Hello world"); const { container } = render( , ); expect(container.querySelector(".renderer-style")).toBeDefined(); expect(container.querySelector(".toolbar-style")).toBeDefined(); expect(container.querySelector(".copy-style")).toBeDefined(); expect(container.querySelector(".edit-style")).toBeDefined(); expect(container.querySelector(".branch-style")).toBeDefined(); }); it("should work with property objects and class strings mixed", () => { const onClick = vi.fn(); const message = createUserMessage("Hello world"); const { container } = render( , ); expect(container.querySelector(".text-lg")).toBeDefined(); const toolbar = container.querySelector(".flex.gap-4"); if (toolbar) { fireEvent.click(toolbar); expect(onClick).toHaveBeenCalled(); } }); it("should correctly display user message content", () => { const message = createUserMessage("This is my message content"); render( , ); expect(screen.getByText("This is my message content")).toBeDefined(); }); }); });