import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { CopilotChatView } from "../CopilotChatView"; import { CopilotChatMessageView } from "../CopilotChatMessageView"; import { CopilotChatInput } from "../CopilotChatInput"; import { CopilotChatSuggestionView } from "../CopilotChatSuggestionView"; 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 sampleMessages = [ { id: "1", role: "user" as const, content: "Hello" }, { id: "2", role: "assistant" as const, content: "Hi there!" }, ]; describe("CopilotChatView Slot System E2E Tests", () => { // ============================================================================ // 1. TAILWIND CLASS TESTS // ============================================================================ describe("1. Tailwind Class Slot Override", () => { describe("messageView slot", () => { it("should apply tailwind class string to messageView", () => { render( , ); // The messageView should have the custom tailwind classes const messageContainer = document.querySelector( '[class*="bg-red-500"]', ); expect(messageContainer).toBeDefined(); expect(messageContainer?.classList.contains("text-white")).toBe(true); expect(messageContainer?.classList.contains("p-4")).toBe(true); }); it("should override default className with tailwind string", () => { const { container } = render( , ); const messageContainer = container.querySelector( ".custom-override-class", ); expect(messageContainer).toBeDefined(); }); }); describe("scrollView slot", () => { it("should apply tailwind class string to scrollView", () => { const { container } = render( , ); const scrollContainer = container.querySelector(".overflow-y-auto"); expect(scrollContainer).toBeDefined(); expect(scrollContainer?.classList.contains("bg-gray-100")).toBe(true); }); }); describe("scrollToBottomButton slot (nested under scrollView)", () => { it("should apply tailwind class string to scrollToBottomButton via scrollView", () => { const { container } = render( , ); // Note: button may only appear when scrolled const button = container.querySelector(".bg-blue-500"); if (button) { expect(button.classList.contains("rounded-full")).toBe(true); } }); }); describe("input slot", () => { it("should apply tailwind class string to input", () => { const { container } = render( , ); const inputContainer = container.querySelector(".border-purple-500"); expect(inputContainer).toBeDefined(); }); }); describe("feather slot (via scrollView)", () => { it("should apply tailwind class string to feather via scrollView", () => { const { container } = render( , ); const feather = container.querySelector(".text-green-500"); if (feather) { expect(feather.classList.contains("font-bold")).toBe(true); } }); }); describe("suggestionView slot", () => { it("should apply tailwind class string to suggestionView", () => { const suggestions = [ { title: "Test", message: "Test message", isLoading: false }, ]; const { container } = render( , ); const suggestionContainer = container.querySelector(".bg-indigo-50"); expect(suggestionContainer).toBeDefined(); }); }); describe("className vs tailwind string precedence", () => { it("tailwind string should completely replace className (not merge)", () => { const { container } = render( , ); const inputEl = container.querySelector(".only-this-class"); expect(inputEl).toBeDefined(); // The string replaces className, so default classes should not be present }); }); describe("non-tailwind inline styles should still work", () => { it("should accept style prop alongside className override", () => { const CustomInput = React.forwardRef( (props, ref) => (
{props.children}
), ); CustomInput.displayName = "CustomInput"; render( , ); const customInput = screen.queryByTestId("custom-input"); if (customInput) { expect(customInput.style.backgroundColor).toBe("rgb(255, 0, 0)"); } }); }); }); // ============================================================================ // 2. PROPERTIES (onClick, disabled, etc.) TESTS // ============================================================================ describe("2. Properties Slot Override", () => { describe("scrollToBottomButton props (nested under scrollView)", () => { it("should pass onClick handler to scrollToBottomButton via scrollView", () => { const handleClick = vi.fn(); render( , ); // Find and click the scroll button if visible const buttons = document.querySelectorAll("button"); buttons.forEach((btn) => { if (btn.getAttribute("aria-label")?.includes("scroll")) { fireEvent.click(btn); } }); // Note: onClick may only fire if button is visible }); it("should pass disabled prop to scrollToBottomButton via scrollView", () => { const { container } = render( , ); const buttons = container.querySelectorAll("button[disabled]"); // Check if any button is disabled expect(buttons).toBeDefined(); }); }); describe("input props", () => { it("should pass onFocus handler to input", async () => { const handleFocus = vi.fn(); render( , ); const textarea = await screen.findByRole("textbox"); fireEvent.focus(textarea); // Note: depends on how input passes props through }); it("should pass autoFocus prop to input", () => { render( , ); // Check if textbox is focused const textarea = document.querySelector("textarea"); // autoFocus behavior may vary }); }); describe("messageView props", () => { it("should pass isRunning prop to messageView", () => { render( , ); const messageView = screen.queryByTestId("message-view-running"); expect(messageView).toBeDefined(); }); }); }); // ============================================================================ // 3. CUSTOM COMPONENT TESTS // ============================================================================ describe("3. Custom Component Slot Override", () => { describe("messageView custom component", () => { it("should render custom messageView component", () => { const CustomMessageView: React.FC = ({ messages }) => (
Custom: {messages?.length || 0} messages
); render( , ); expect(screen.getByTestId("custom-message-view")).toBeDefined(); expect(screen.getByText(/Custom: 2 messages/)).toBeDefined(); }); it("custom messageView should receive all props including messages", () => { const receivedProps: any = {}; const CustomMessageView: React.FC = (props) => { Object.assign(receivedProps, props); return
Received
; }; render( , ); expect(receivedProps.messages).toBeDefined(); expect(receivedProps.messages.length).toBe(2); expect(receivedProps.isRunning).toBe(true); }); }); describe("input custom component", () => { it("should render custom input component", () => { const CustomInput: React.FC = (props) => (
props.onChange?.(e.target.value)} />
); render( , ); expect(screen.getByTestId("custom-input")).toBeDefined(); expect(screen.getByPlaceholderText("Custom input")).toBeDefined(); }); it("custom input should receive onSubmitMessage callback", () => { const submitHandler = vi.fn(); const CustomInput: React.FC = ({ onSubmitMessage }) => ( ); render( , ); fireEvent.click(screen.getByTestId("custom-submit")); expect(submitHandler).toHaveBeenCalledWith("test message"); }); }); describe("scrollView custom component", () => { it("should render custom scrollView component", () => { const CustomScrollView: React.FC = ({ children }) => (
{children}
); render( , ); expect(screen.getByTestId("custom-scroll")).toBeDefined(); }); }); describe("suggestionView custom component", () => { it("should render custom suggestionView component", () => { const suggestions = [ { title: "Option A", message: "Do A", isLoading: false }, { title: "Option B", message: "Do B", isLoading: false }, ]; const CustomSuggestionView: React.FC = ({ suggestions, onSelectSuggestion, }) => (
{suggestions.map((s: any, i: number) => ( ))}
); render( , ); expect(screen.getByTestId("custom-suggestions")).toBeDefined(); expect(screen.getByText("Option A")).toBeDefined(); expect(screen.getByText("Option B")).toBeDefined(); }); }); describe("feather custom component (via scrollView)", () => { it("should render custom feather component via scrollView", () => { const CustomFeather: React.FC = () => (
Custom Feather
); render( , ); const feather = screen.queryByTestId("custom-feather"); if (feather) { expect(feather.textContent).toContain("Custom Feather"); } }); }); describe("scrollToBottomButton custom component (nested under scrollView)", () => { it("should render custom scrollToBottomButton component via scrollView", () => { const CustomScrollButton: React.FC = ({ onClick }) => ( ); render( , ); // Button may only appear when scrolled const btn = screen.queryByTestId("custom-scroll-btn"); if (btn) { expect(btn.textContent).toContain("Go Down"); } }); }); }); // ============================================================================ // 4. RECURSIVE DRILL-DOWN TESTS // ============================================================================ describe("4. Recursive Subcomponent Drill-Down", () => { describe("messageView -> assistantMessage drill-down", () => { it("should allow customizing assistantMessage within messageView", () => { const CustomAssistantMessage: React.FC = ({ message }) => (
Custom Assistant: {message?.content}
); render( , ); expect(screen.getByTestId("custom-assistant-msg")).toBeDefined(); expect(screen.getByText(/Custom Assistant: Hi there!/)).toBeDefined(); }); }); describe("messageView -> userMessage drill-down", () => { it("should allow customizing userMessage within messageView", () => { const CustomUserMessage: React.FC = ({ message }) => (
User said: {message?.content}
); render( , ); expect(screen.getByTestId("custom-user-msg")).toBeDefined(); expect(screen.getByText(/User said: Hello/)).toBeDefined(); }); }); describe("messageView -> cursor drill-down", () => { it("should allow customizing cursor within messageView", () => { const CustomCursor: React.FC = () => ( | ); render( , ); // Cursor appears when running const cursor = screen.queryByTestId("custom-cursor"); if (cursor) { expect(cursor.textContent).toBe("|"); } }); }); describe("input -> textArea drill-down", () => { it("should allow customizing textArea within input", () => { const CustomTextArea: React.FC = React.forwardRef< HTMLTextAreaElement, any >(({ value, onChange, ...props }, ref) => (