import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { CopilotPopupView } from "../CopilotPopupView";
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("CopilotPopupView Slot System E2E Tests", () => {
// ============================================================================
// 1. TAILWIND CLASS TESTS - HEADER SLOT (UNIQUE TO POPUP)
// ============================================================================
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-indigo-500");
if (header) {
expect(header.classList.contains("text-white")).toBe(true);
expect(header.classList.contains("shadow-lg")).toBe(true);
}
});
it("should override default header styles", () => {
const { container } = render(
,
);
const header = container.querySelector(".rounded-t-3xl");
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-popup-header");
expect(header).toBeDefined();
});
it("should pass title prop through to header", () => {
render(
,
);
expect(screen.queryByText("Popup Assistant")).toBeDefined();
});
});
});
// ============================================================================
// 3. CUSTOM COMPONENT TESTS - HEADER SLOT
// ============================================================================
describe("3. Custom Component - Header Slot", () => {
it("should allow custom component for header", () => {
const CustomHeader: React.FC = () => (
);
render(
,
);
const custom = screen.queryByTestId("custom-popup-header-component");
expect(custom).toBeDefined();
expect(custom?.textContent).toContain("AI Assistant");
});
it("should allow passing header props for customization", () => {
const { container } = render(
,
);
expect(screen.queryByText("Chat Popup")).toBeDefined();
expect(container.querySelector(".text-lg")).toBeDefined();
expect(container.querySelector(".italic")).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-slate-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-indigo-300");
expect(input).toBeDefined();
});
});
describe("scrollView slot (inherited)", () => {
it("should apply tailwind class string to inherited scrollView", () => {
const { container } = render(
,
);
const scrollView = container.querySelector(".scrollbar-thin");
expect(scrollView).toBeDefined();
});
});
describe("suggestionView slot (inherited)", () => {
it("should apply tailwind class string to inherited suggestionView", () => {
const suggestions = [
{ title: "Quick Reply", message: "Reply message", isLoading: false },
];
const { container } = render(
,
);
const suggestionView = container.querySelector(".flex-wrap");
if (suggestionView) {
expect(suggestionView.classList.contains("gap-2")).toBe(true);
}
});
});
describe("input slot (inherited)", () => {
it("should apply tailwind class string to inherited input", () => {
const { container } = render(
,
);
const input = container.querySelector(".bg-gray-100");
expect(input).toBeDefined();
});
});
});
// ============================================================================
// 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-xl");
expect(titleContent).toBeDefined();
expect(titleContent?.classList.contains("text-indigo-600")).toBe(true);
expect(titleContent?.classList.contains("tracking-wide")).toBe(true);
});
it("should allow customizing header closeButton through props object", () => {
const { container } = render(
,
);
const closeBtn = container.querySelector(".popup-close-btn");
expect(closeBtn).toBeDefined();
});
it("should allow custom component for header via component slot", () => {
const CustomHeader: React.FC = () => (
Custom Popup Header
);
render(
,
);
const customClose = screen.queryByTestId("popup-custom-close");
expect(customClose).toBeDefined();
expect(customClose?.textContent).toBe("Dismiss");
});
it("should allow custom layout via custom header component", () => {
const CustomLayoutHeader: React.FC = () => (
);
const { container } = render(
,
);
expect(
container.querySelector(".custom-popup-header-layout"),
).toBeDefined();
expect(container.querySelector(".close-area")).toBeDefined();
expect(container.querySelector(".title-area")).toBeDefined();
});
});
// ============================================================================
// 6. CLASSNAME AND MIXED CUSTOMIZATION
// ============================================================================
describe("6. className Override and Mixed Customization", () => {
it("should apply className to popup root", () => {
const { container } = render(
,
);
const popup = container.querySelector(".custom-popup-class");
expect(popup).toBeDefined();
});
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();
}
});
});
// ============================================================================
// 7. POPUP-SPECIFIC PROPS
// ============================================================================
describe("7. Popup-specific Props", () => {
it("should support custom width prop", () => {
const { container } = render(
,
);
const popup = container.querySelector("[data-copilot-popup]");
if (popup) {
expect(popup.getAttribute("style")).toContain("--copilot-popup-width");
}
});
it("should support custom height prop", () => {
const { container } = render(
,
);
const popup = container.querySelector("[data-copilot-popup]");
if (popup) {
expect(popup.getAttribute("style")).toContain("--copilot-popup-height");
}
});
it("should support string dimensions", () => {
const { container } = render(
,
);
const popup = container.querySelector("[data-copilot-popup]");
expect(popup).toBeDefined();
});
it("should support clickOutsideToClose prop", () => {
const { container } = render(
,
);
const popup = container.querySelector("[data-copilot-popup]");
expect(popup).toBeDefined();
});
});
// ============================================================================
// 8. INTEGRATION TESTS
// ============================================================================
describe("8. Integration Tests", () => {
it("should render popup with all default components when open", () => {
const { container } = render(
,
);
const popup = container.querySelector("[data-copilot-popup]");
expect(popup).toBeDefined();
expect(
container.querySelector('[data-slot="copilot-modal-header"]'),
).toBeDefined();
});
it("should render messages in popup", () => {
render(
,
);
expect(screen.queryByText("Hello")).toBeDefined();
expect(screen.queryByText("Hi there!")).toBeDefined();
});
it("should handle empty messages array", () => {
const { container } = render(
,
);
const popup = container.querySelector("[data-copilot-popup]");
expect(popup).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();
});
it("should not render popup content when closed", () => {
// Render without the outer CopilotChatConfigurationProvider so the
// PopupView's own provider (with defaultOpen=false) isn't overridden
// by a parent whose isModalOpen defaults to true.
const { container } = render(
,
);
// Popup should not be rendered when closed
const popup = container.querySelector("[data-copilot-popup]");
expect(popup).toBeNull();
});
});
// ============================================================================
// 9. TOGGLE BUTTON SLOT TESTS
// ============================================================================
describe("9. 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-purple-500");
expect(toggleButton).toBeDefined();
expect(toggleButton?.classList.contains("hover:bg-purple-600")).toBe(
true,
);
});
});
describe("toggleButton slot - Props object", () => {
it("should pass custom props to toggle button", () => {
const { container } = render(
,
);
const toggleButton = screen.queryByTestId("popup-custom-toggle");
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("popup-custom-toggle-component");
expect(custom).toBeDefined();
expect(custom?.textContent).toBe("Open Chat");
});
});
});
});