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