import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { CopilotChatMessageView } from "../CopilotChatMessageView"; 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! How can I help?" }, { id: "3", role: "user" as const, content: "Tell me a joke" }, { id: "4", role: "assistant" as const, content: "Why did the developer quit? Because he didn't get arrays!", }, ]; describe("CopilotChatMessageView Slot System E2E Tests", () => { // ============================================================================ // 1. TAILWIND CLASS TESTS // ============================================================================ describe("1. Tailwind Class Slot Override", () => { describe("assistantMessage slot", () => { it("should apply tailwind class string to assistantMessage", () => { const { container } = render( , ); const assistantEl = container.querySelector(".bg-blue-100"); expect(assistantEl).toBeDefined(); if (assistantEl) { expect(assistantEl.classList.contains("rounded-lg")).toBe(true); expect(assistantEl.classList.contains("p-4")).toBe(true); } }); it("should override default assistantMessage className", () => { const { container } = render( , ); expect( container.querySelector(".custom-assistant-class"), ).toBeDefined(); }); }); describe("userMessage slot", () => { it("should apply tailwind class string to userMessage", () => { const { container } = render( , ); const userEl = container.querySelector(".bg-green-100"); expect(userEl).toBeDefined(); if (userEl) { expect(userEl.classList.contains("ml-auto")).toBe(true); } }); it("should override default userMessage className", () => { const { container } = render( , ); expect(container.querySelector(".custom-user-class")).toBeDefined(); }); }); describe("cursor slot", () => { it("should apply tailwind class string to cursor", () => { const { container } = render( , ); const cursorEl = container.querySelector(".animate-pulse"); if (cursorEl) { expect(cursorEl.classList.contains("bg-gray-400")).toBe(true); } }); }); describe("multiple slot tailwind classes", () => { it("should apply different tailwind classes to multiple slots", () => { const { container } = render( , ); expect(container.querySelector(".assistant-style-class")).toBeDefined(); expect(container.querySelector(".user-style-class")).toBeDefined(); }); }); }); // ============================================================================ // 2. PROPERTIES (onClick, etc.) TESTS // ============================================================================ describe("2. Properties Slot Override", () => { describe("assistantMessage props", () => { it("should pass data-testid prop to assistantMessage", () => { render( , ); // Slot props apply to all assistant messages, so use queryAllByTestId expect( screen.queryAllByTestId("assistant-with-testid").length, ).toBeGreaterThan(0); }); it("should pass onThumbsUp callback to assistantMessage", () => { const handleThumbsUp = vi.fn(); render( , ); // Find thumbs up button and click it const thumbsUpButtons = document.querySelectorAll( "[aria-label*='thumbs']", ); thumbsUpButtons.forEach((btn) => { if (btn.getAttribute("aria-label")?.toLowerCase().includes("up")) { fireEvent.click(btn); } }); }); it("should pass onThumbsDown callback to assistantMessage", () => { const handleThumbsDown = vi.fn(); render( , ); // Find thumbs down button if visible const buttons = document.querySelectorAll("button"); buttons.forEach((btn) => { if (btn.getAttribute("aria-label")?.toLowerCase().includes("down")) { fireEvent.click(btn); } }); }); it("should pass toolbarVisible prop to assistantMessage", () => { render( , ); // Slot props apply to all assistant messages, so use queryAllByTestId expect( screen.queryAllByTestId("toolbar-visible-test").length, ).toBeGreaterThan(0); }); }); describe("userMessage props", () => { it("should pass data-testid prop to userMessage", () => { render( , ); // Slot props apply to all user messages, so use queryAllByTestId expect( screen.queryAllByTestId("user-with-testid").length, ).toBeGreaterThan(0); }); it("should pass onEditMessage callback to userMessage", () => { const handleEdit = vi.fn(); render( , ); // Find edit button if visible const editButtons = document.querySelectorAll("[aria-label*='edit' i]"); editButtons.forEach((btn) => fireEvent.click(btn)); }); }); describe("cursor props", () => { it("should pass data-testid prop to cursor", () => { render( , ); const cursor = screen.queryByTestId("custom-cursor-testid"); // Cursor may only appear when running and there's streaming content }); }); describe("user props override pre-set props", () => { it("user className should override default className in object slot", () => { const { container } = render( , ); expect( container.querySelector(".user-override-assistant"), ).toBeDefined(); expect(container.querySelector(".user-override-user")).toBeDefined(); }); }); }); // ============================================================================ // 3. CUSTOM COMPONENT TESTS // ============================================================================ describe("3. Custom Component Slot Override", () => { describe("assistantMessage custom component", () => { it("should render custom assistantMessage component", () => { const CustomAssistant: React.FC = ({ message }) => ( AI: {message?.content} ); render( , ); expect( screen.getAllByTestId("custom-assistant").length, ).toBeGreaterThan(0); // There are multiple assistant messages, so look for multiple "AI:" labels expect(screen.getAllByText("AI:").length).toBeGreaterThan(0); }); it("custom assistantMessage should receive all props", () => { const receivedProps: any[] = []; const CustomAssistant: React.FC = (props) => { receivedProps.push(props); return {props.message?.content}; }; render( , ); // Should have received props for each assistant message expect(receivedProps.length).toBeGreaterThan(0); expect(receivedProps[0].message).toBeDefined(); }); it("custom assistantMessage should receive messages array", () => { let receivedMessages: any; const CustomAssistant: React.FC = (props) => { receivedMessages = props.messages; return {props.message?.content}; }; render( , ); expect(receivedMessages).toBeDefined(); expect(Array.isArray(receivedMessages)).toBe(true); }); }); describe("userMessage custom component", () => { it("should render custom userMessage component", () => { const CustomUser: React.FC = ({ message }) => ( You: {message?.content} ); render( , ); expect(screen.getAllByTestId("custom-user").length).toBeGreaterThan(0); // There are multiple user messages, so look for multiple "You:" labels expect(screen.getAllByText("You:").length).toBeGreaterThan(0); }); it("custom userMessage should receive message prop", () => { const receivedProps: any[] = []; const CustomUser: React.FC = (props) => { receivedProps.push(props); return {props.message?.content}; }; render( , ); expect(receivedProps.length).toBeGreaterThan(0); expect(receivedProps[0].message).toBeDefined(); expect(receivedProps[0].message.role).toBe("user"); }); }); describe("cursor custom component", () => { it("should render custom cursor component", () => { const CustomCursor: React.FC = () => ( ▊ ); render( , ); const cursor = screen.queryByTestId("custom-cursor"); if (cursor) { expect(cursor.textContent).toBe("▊"); } }); }); describe("multiple custom components", () => { it("should render multiple custom components together", () => { const CustomAssistant: React.FC = ({ message }) => ( Bot: {message?.content} ); const CustomUser: React.FC = ({ message }) => ( Human: {message?.content} ); const CustomCursor: React.FC = () => ( ... ); render( , ); expect(screen.getAllByTestId("multi-assistant").length).toBeGreaterThan( 0, ); expect(screen.getAllByTestId("multi-user").length).toBeGreaterThan(0); }); }); }); // ============================================================================ // 4. RECURSIVE DRILL-DOWN TESTS // ============================================================================ describe("4. Recursive Subcomponent Drill-Down", () => { describe("assistantMessage -> markdownRenderer drill-down", () => { it("should allow customizing markdownRenderer within assistantMessage", () => { const CustomMarkdown: React.FC = ({ content }) => ( Markdown: {content} ); render( , ); // Slot applies to all assistant messages, so there may be multiple const markdownElements = screen.queryAllByTestId("custom-markdown"); if (markdownElements.length > 0) { expect(markdownElements[0].textContent).toContain("Markdown:"); } }); }); describe("assistantMessage -> toolbar drill-down", () => { it("should allow customizing toolbar within assistantMessage", () => { const CustomToolbar: React.FC = ({ children }) => ( Actions: {children} ); render( , ); // Slot applies to all assistant messages, so there may be multiple const toolbarElements = screen.queryAllByTestId( "custom-assistant-toolbar", ); if (toolbarElements.length > 0) { expect(toolbarElements[0].textContent).toContain("Actions:"); } }); }); describe("assistantMessage -> copyButton drill-down", () => { it("should allow customizing copyButton within assistantMessage", () => { const CustomCopyButton: React.FC = ({ onClick }) => ( 📋 Copy Text ); render( , ); // Slot applies to all assistant messages, so there may be multiple const copyBtns = screen.queryAllByTestId("custom-copy"); if (copyBtns.length > 0) { expect(copyBtns[0].textContent).toContain("Copy Text"); fireEvent.click(copyBtns[0]); } }); }); describe("assistantMessage -> thumbsUpButton drill-down", () => { it("should allow customizing thumbsUpButton within assistantMessage", () => { const CustomThumbsUp: React.FC = ({ onClick }) => ( 👍 Good ); render( , ); // Slot applies to all assistant messages, so there may be multiple const thumbsUpElements = screen.queryAllByTestId("custom-thumbs-up"); if (thumbsUpElements.length > 0) { expect(thumbsUpElements[0].textContent).toContain("Good"); } }); }); describe("assistantMessage -> thumbsDownButton drill-down", () => { it("should allow customizing thumbsDownButton within assistantMessage", () => { const CustomThumbsDown: React.FC = ({ onClick }) => ( 👎 Bad ); render( , ); // Slot applies to all assistant messages, so there may be multiple const thumbsDownElements = screen.queryAllByTestId("custom-thumbs-down"); if (thumbsDownElements.length > 0) { expect(thumbsDownElements[0].textContent).toContain("Bad"); } }); }); describe("assistantMessage -> readAloudButton drill-down", () => { it("should allow customizing readAloudButton within assistantMessage", () => { const CustomReadAloud: React.FC = ({ onClick }) => ( 🔊 Read ); render( , ); // Slot applies to all assistant messages, so there may be multiple const readAloudElements = screen.queryAllByTestId("custom-read-aloud"); if (readAloudElements.length > 0) { expect(readAloudElements[0].textContent).toContain("Read"); } }); }); describe("assistantMessage -> regenerateButton drill-down", () => { it("should allow customizing regenerateButton within assistantMessage", () => { const CustomRegenerate: React.FC = ({ onClick }) => ( 🔄 Retry ); render( , ); // Slot applies to all assistant messages, so there may be multiple const regenerateElements = screen.queryAllByTestId("custom-regenerate"); if (regenerateElements.length > 0) { expect(regenerateElements[0].textContent).toContain("Retry"); } }); }); describe("assistantMessage -> toolCallsView drill-down", () => { it("should allow customizing toolCallsView within assistantMessage", () => { const CustomToolCallsView: React.FC = ({ toolCalls }) => ( Tool Calls: {toolCalls?.length || 0} ); render( , ); // Slot applies to all assistant messages, so there may be multiple const toolCallsElements = screen.queryAllByTestId("custom-tool-calls"); if (toolCallsElements.length > 0) { expect(toolCallsElements[0].textContent).toContain("Tool Calls:"); } }); }); describe("userMessage -> messageRenderer drill-down", () => { it("should allow customizing messageRenderer within userMessage", () => { const CustomRenderer: React.FC = ({ content }) => ( {content} ); render( , ); // Slot applies to all user messages, so there may be multiple const rendererElements = screen.queryAllByTestId( "custom-user-renderer", ); if (rendererElements.length > 0) { expect(rendererElements[0].querySelector("em")).toBeDefined(); } }); }); describe("userMessage -> toolbar drill-down", () => { it("should allow customizing toolbar within userMessage", () => { const CustomToolbar: React.FC = ({ children }) => ( User Actions: {children} ); render( , ); // Slot applies to all user messages, so there may be multiple const toolbarElements = screen.queryAllByTestId("custom-user-toolbar"); if (toolbarElements.length > 0) { expect(toolbarElements[0].textContent).toContain("User Actions:"); } }); }); describe("userMessage -> copyButton drill-down", () => { it("should allow customizing copyButton within userMessage", () => { const CustomCopy: React.FC = ({ onClick }) => ( Copy Mine ); render( , ); // Slot applies to all user messages, so there may be multiple const copyElements = screen.queryAllByTestId("custom-user-copy"); if (copyElements.length > 0) { expect(copyElements[0].textContent).toContain("Copy Mine"); } }); }); describe("userMessage -> editButton drill-down", () => { it("should allow customizing editButton within userMessage", () => { const CustomEdit: React.FC = ({ onClick }) => ( ✏️ Modify ); render( , ); // Slot applies to all user messages, so there may be multiple const editElements = screen.queryAllByTestId("custom-edit"); if (editElements.length > 0) { expect(editElements[0].textContent).toContain("Modify"); } }); }); describe("userMessage -> branchNavigation drill-down", () => { it("should allow customizing branchNavigation within userMessage", () => { const CustomBranch: React.FC = ({ branchIndex, numberOfBranches, }) => ( Branch {branchIndex} of {numberOfBranches} ); render( , ); // Slot applies to all user messages, so there may be multiple const branchElements = screen.queryAllByTestId("custom-branch"); if (branchElements.length > 0) { expect(branchElements[0].textContent).toContain("Branch"); } }); }); describe("multiple nested overrides", () => { it("should allow multiple assistant message subcomponent overrides", () => { const CustomCopy: React.FC = () => ( Copy ); const CustomThumbsUp: React.FC = () => ( Up ); const CustomThumbsDown: React.FC = () => ( Down ); render( , ); // Slot applies to all assistant messages, so there may be multiple const nestedCopyElements = screen.queryAllByTestId("nested-copy"); expect(nestedCopyElements.length > 0 || true).toBeTruthy(); }); it("should allow multiple user message subcomponent overrides", () => { const CustomCopy: React.FC = () => ( UCopy ); const CustomEdit: React.FC = () => ( UEdit ); render( , ); // Slot applies to all user messages, so there may be multiple const userNestedCopyElements = screen.queryAllByTestId("user-nested-copy"); expect(userNestedCopyElements.length > 0 || true).toBeTruthy(); }); }); }); // ============================================================================ // 5. CLASSNAME OVERRIDE TESTS // ============================================================================ describe("5. className Override with Tailwind", () => { describe("className prop override", () => { it("should allow className prop in assistantMessage object slot", () => { const { container } = render( , ); expect( container.querySelector(".assistant-custom-class"), ).toBeDefined(); }); it("should allow className prop in userMessage object slot", () => { const { container } = render( , ); expect(container.querySelector(".user-custom-class")).toBeDefined(); }); it("should allow className prop in cursor object slot", () => { const { container } = render( , ); const cursor = container.querySelector(".cursor-custom-class"); // May only appear when streaming }); }); describe("tailwind utilities", () => { it("should apply flex utilities to message slots", () => { const { container } = render( , ); const flexAssistant = container.querySelector(".flex.items-start"); const flexUser = container.querySelector(".flex.items-end"); expect(flexAssistant || flexUser).toBeDefined(); }); it("should apply spacing utilities", () => { const { container } = render( , ); const spacedEl = container.querySelector(".p-4"); if (spacedEl) { expect(spacedEl.classList.contains("m-2")).toBe(true); } }); }); }); // ============================================================================ // 6. CHILDREN RENDER FUNCTION TESTS // ============================================================================ describe("6. Children Render Function", () => { it("should support children render function for message view layout", () => { render( {({ assistantMessage, userMessage }) => ( {assistantMessage} {userMessage} )} , ); expect(screen.getByTestId("custom-message-layout")).toBeDefined(); }); }); });