import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { CopilotSidebarView } from "../CopilotSidebarView";
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("CopilotSidebarView Slot System E2E Tests", () => {
// ============================================================================
// 1. TAILWIND CLASS TESTS - HEADER SLOT (UNIQUE TO SIDEBAR)
// ============================================================================
describe("1. Tailwind Class Slot Override - Header Slot", () => {
describe("header slot", () => {
it("should apply tailwind class string to header", () => {
const { container } = render(
,
);
const header = container.querySelector(".bg-gradient-to-r");
if (header) {
expect(header.classList.contains("from-blue-500")).toBe(true);
expect(header.classList.contains("text-white")).toBe(true);
}
});
it("should override default header border with custom styles", () => {
const { container } = render(
,
);
const header = container.querySelector(".border-b-2");
expect(header).toBeDefined();
});
});
});
// ============================================================================
// 2. PROPERTY PASSING TESTS - HEADER SLOT
// ============================================================================
describe("2. Property Passing - Header Slot", () => {
describe("header slot", () => {
it("should pass custom props to header", () => {
const { container } = render(
,
);
const header = screen.queryByTestId("custom-sidebar-header");
expect(header).toBeDefined();
});
it("should pass title prop through to header", () => {
render(
,
);
// The title should be rendered in the header
expect(screen.queryByText("My Sidebar Chat")).toBeDefined();
});
});
});
// ============================================================================
// 3. CUSTOM COMPONENT TESTS - HEADER SLOT
// ============================================================================
describe("3. Custom Component - Header Slot", () => {
it("should allow custom component for header", () => {
const CustomHeader: React.FC = () => (
Custom Sidebar Header
);
render(
,
);
const custom = screen.queryByTestId("custom-header-component");
expect(custom).toBeDefined();
expect(custom?.textContent).toContain("Custom Sidebar Header");
});
it("should allow passing header props for customization", () => {
const { container } = render(
,
);
expect(screen.queryByText("Customized Header")).toBeDefined();
expect(container.querySelector(".text-xl")).toBeDefined();
});
});
// ============================================================================
// 4. INHERITED COPILOTCHATVIEW SLOTS
// ============================================================================
describe("4. Inherited CopilotChatView Slots", () => {
describe("messageView slot (inherited)", () => {
it("should apply tailwind class string to inherited messageView", () => {
const { container } = render(
,
);
const messageView = container.querySelector(".bg-gray-50");
expect(messageView).toBeDefined();
});
});
describe("input slot (inherited)", () => {
it("should apply tailwind class string to inherited input", () => {
const { container } = render(
,
);
const input = container.querySelector(".border-blue-400");
expect(input).toBeDefined();
});
});
describe("scrollView slot (inherited)", () => {
it("should apply tailwind class string to inherited scrollView", () => {
const { container } = render(
,
);
const scrollView = container.querySelector(".overflow-y-scroll");
expect(scrollView).toBeDefined();
});
});
describe("suggestionView slot (inherited)", () => {
it("should apply tailwind class string to inherited suggestionView", () => {
const suggestions = [
{ title: "Test", message: "Test message", isLoading: false },
];
const { container } = render(
,
);
const suggestionView = container.querySelector(".gap-4");
if (suggestionView) {
expect(suggestionView.classList.contains("p-2")).toBe(true);
}
});
});
});
// ============================================================================
// 5. DRILL-DOWN INTO HEADER SUB-SLOTS
// ============================================================================
describe("5. Drill-down into Header Sub-slots", () => {
it("should allow customizing header titleContent through props object", () => {
const { container } = render(
,
);
const titleContent = container.querySelector(".text-2xl");
expect(titleContent).toBeDefined();
expect(titleContent?.classList.contains("text-purple-600")).toBe(true);
});
it("should allow customizing header closeButton through props object", () => {
const { container } = render(
,
);
const closeBtn = container.querySelector(".custom-close-btn");
expect(closeBtn).toBeDefined();
});
it("should allow custom component for header via component slot", () => {
const CustomHeader: React.FC = () => (
);
render(
,
);
const customClose = screen.queryByTestId("sidebar-custom-close");
expect(customClose).toBeDefined();
expect(customClose?.textContent).toBe("← Back");
});
});
// ============================================================================
// 6. CLASSNAME AND MIXED CUSTOMIZATION
// ============================================================================
describe("6. className Override and Mixed Customization", () => {
it("should merge multiple slot classNames correctly", () => {
const { container } = render(
,
);
expect(container.querySelector(".header-style")).toBeDefined();
expect(container.querySelector(".message-style")).toBeDefined();
expect(container.querySelector(".input-style")).toBeDefined();
});
it("should work with property objects and class strings mixed", () => {
const onClick = vi.fn();
const { container } = render(
,
);
expect(container.querySelector(".styled-input")).toBeDefined();
const header = container.querySelector(".clickable-header");
if (header) {
fireEvent.click(header);
expect(onClick).toHaveBeenCalled();
}
});
it("should support custom width prop", () => {
const { container } = render(
,
);
const sidebar = container.querySelector("[data-copilot-sidebar]");
expect(sidebar).toBeDefined();
// Width should be applied via CSS custom property
expect(sidebar?.getAttribute("style")).toContain("--sidebar-width");
});
it("should support string width prop", () => {
const { container } = render(
,
);
const sidebar = container.querySelector("[data-copilot-sidebar]");
expect(sidebar).toBeDefined();
});
});
// ============================================================================
// 7. INTEGRATION TESTS
// ============================================================================
describe("7. Integration Tests", () => {
it("should render sidebar with all default components", () => {
const { container } = render(
,
);
const sidebar = container.querySelector("[data-copilot-sidebar]");
expect(sidebar).toBeDefined();
// Should have header
expect(
container.querySelector('[data-slot="copilot-modal-header"]'),
).toBeDefined();
});
it("should render messages in sidebar", () => {
render(
,
);
expect(screen.queryByText("Hello")).toBeDefined();
expect(screen.queryByText("Hi there!")).toBeDefined();
});
it("should handle empty messages array", () => {
const { container } = render(
,
);
const sidebar = container.querySelector("[data-copilot-sidebar]");
expect(sidebar).toBeDefined();
});
it("should combine header customization with inherited slot customization", () => {
const { container } = render(
,
);
expect(container.querySelector(".custom-header-root")).toBeDefined();
expect(container.querySelector(".custom-title")).toBeDefined();
expect(container.querySelector(".custom-message")).toBeDefined();
expect(container.querySelector(".custom-input")).toBeDefined();
expect(container.querySelector(".custom-scroll")).toBeDefined();
});
});
// ============================================================================
// 8. TOGGLE BUTTON SLOT TESTS
// ============================================================================
describe("8. Toggle Button Slot", () => {
describe("toggleButton slot - Tailwind class string", () => {
it("should apply tailwind class string to toggle button", () => {
const { container } = render(
,
);
const toggleButton = container.querySelector(".bg-red-500");
expect(toggleButton).toBeDefined();
expect(toggleButton?.classList.contains("hover:bg-red-600")).toBe(true);
});
});
describe("toggleButton slot - Props object", () => {
it("should pass custom props to toggle button", () => {
const { container } = render(
,
);
const toggleButton = screen.queryByTestId("custom-toggle-button");
expect(toggleButton).toBeDefined();
});
it("should pass openIcon and closeIcon sub-slot props", () => {
const { container } = render(
,
);
// The icons should have custom classes applied
const openIconSlot = container.querySelector(
'[data-slot="chat-toggle-button-open-icon"]',
);
const closeIconSlot = container.querySelector(
'[data-slot="chat-toggle-button-close-icon"]',
);
expect(openIconSlot).toBeDefined();
expect(closeIconSlot).toBeDefined();
});
});
describe("toggleButton slot - Custom component", () => {
it("should allow custom component for toggle button", () => {
const CustomToggleButton: React.FC = () => (
);
render(
,
);
const custom = screen.queryByTestId("custom-toggle-component");
expect(custom).toBeDefined();
expect(custom?.textContent).toBe("Toggle Chat");
});
});
});
});