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 = () => (
AI Assistant
); 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 = () => (
Custom Title
); 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"); }); }); }); });