import { render, screen, fireEvent } from "@testing-library/vue"; import { defineComponent } from "vue"; import { describe, it, expect, vi } from "vitest"; import type { AssistantMessage } from "@ag-ui/core"; import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue"; import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue"; import CopilotChatAssistantMessage from "../CopilotChatAssistantMessage.vue"; const TestWrapper = defineComponent({ components: { CopilotKitProvider, CopilotChatConfigurationProvider, }, template: ` `, }); function createAssistantMessage(content: string): AssistantMessage { return { id: "msg-1", role: "assistant", content, } as AssistantMessage; } function renderInWrapper(component: ReturnType) { const Wrapped = defineComponent({ components: { TestWrapper, UnderTest: component }, template: ` `, }); return render(Wrapped); } describe("CopilotChatAssistantMessage Slot System E2E Tests", () => { describe("1. Tailwind Class Slot Override", () => { describe("markdownRenderer slot", () => { it("should apply tailwind class string to markdownRenderer", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message] }; }, template: ` {{ content }} `, }); renderInWrapper(Host); const markdown = screen.getByTestId("markdown-renderer"); expect(markdown.classList.contains("bg-blue-100")).toBe(true); expect(markdown.classList.contains("rounded-lg")).toBe(true); }); }); describe("toolbar slot", () => { it("should apply tailwind class string to toolbar", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onThumbsUp: vi.fn() }; }, template: ` Toolbar `, }); renderInWrapper(Host); const toolbar = screen.getByTestId("toolbar"); expect(toolbar.classList.contains("bg-gray-100")).toBe(true); expect(toolbar.classList.contains("border-t")).toBe(true); }); }); describe("copyButton slot", () => { it("should apply tailwind class string to copyButton", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message] }; }, template: ` Copy `, }); renderInWrapper(Host); expect( screen.getByTestId("copy-btn").classList.contains("text-green-500"), ).toBe(true); }); }); describe("thumbsUpButton slot", () => { it("should apply tailwind class string to thumbsUpButton", () => { const onThumbsUp = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onThumbsUp }; }, template: ` Up `, }); renderInWrapper(Host); expect( screen .getByTestId("thumbs-up-btn") .classList.contains("text-blue-500"), ).toBe(true); }); }); describe("thumbsDownButton slot", () => { it("should apply tailwind class string to thumbsDownButton", () => { const onThumbsDown = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onThumbsDown }; }, template: ` Down `, }); renderInWrapper(Host); expect( screen .getByTestId("thumbs-down-btn") .classList.contains("text-red-500"), ).toBe(true); }); }); describe("readAloudButton slot", () => { it("should apply tailwind class string to readAloudButton", () => { const onReadAloud = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onReadAloud }; }, template: ` Read `, }); renderInWrapper(Host); expect( screen .getByTestId("read-aloud-btn") .classList.contains("text-purple-500"), ).toBe(true); }); }); describe("regenerateButton slot", () => { it("should apply tailwind class string to regenerateButton", () => { const onRegenerate = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onRegenerate }; }, template: ` Regenerate `, }); renderInWrapper(Host); expect( screen .getByTestId("regenerate-btn") .classList.contains("text-orange-500"), ).toBe(true); }); }); describe("toolCallsView slot", () => { it("should apply tailwind class string to toolCallsView", () => { const message: AssistantMessage = { ...createAssistantMessage("Hello"), toolCalls: [ { id: "tc-1", type: "function", function: { name: "test_tool", arguments: "{}" }, } as any, ], }; const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { return { message, messages: [message] }; }, template: ` Tools `, }); renderInWrapper(Host); const toolCalls = screen.queryByTestId("tool-calls-view"); if (toolCalls) { expect(toolCalls.classList.contains("p-2")).toBe(true); } }); }); }); describe("2. Property Passing (onClick, disabled, etc.)", () => { describe("markdownRenderer slot", () => { it("should pass custom props to markdownRenderer", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message] }; }, template: ` {{ content }} `, }); renderInWrapper(Host); expect(screen.queryByTestId("custom-markdown")).toBeDefined(); }); }); describe("toolbar slot", () => { it("should pass custom onClick to toolbar", async () => { const onClick = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onClick, onThumbsUp: vi.fn(), }; }, template: ` Toolbar `, }); renderInWrapper(Host); await fireEvent.click(screen.getByTestId("custom-toolbar")); expect(onClick).toHaveBeenCalled(); }); }); describe("copyButton slot", () => { it("should pass custom onClick that wraps default behavior", async () => { const customOnClick = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], customOnClick }; }, template: ` Copy `, }); renderInWrapper(Host); await fireEvent.click(screen.getByTestId("custom-copy-button")); expect(customOnClick).toHaveBeenCalled(); }); it("should support disabled state on copyButton", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message] }; }, template: ` Copy `, }); renderInWrapper(Host); expect( screen.getByTestId("disabled-copy-button").hasAttribute("disabled"), ).toBe(true); }); }); describe("thumbsUpButton slot", () => { it("should call custom onClick on thumbsUpButton", async () => { const customOnClick = vi.fn(); const onThumbsUp = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], customOnClick, onThumbsUp }; }, template: ` Up `, }); renderInWrapper(Host); await fireEvent.click(screen.getByTestId("thumbs-up-custom")); expect(customOnClick).toHaveBeenCalled(); }); }); describe("thumbsDownButton slot", () => { it("should call custom onClick on thumbsDownButton", async () => { const customOnClick = vi.fn(); const onThumbsDown = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], customOnClick, onThumbsDown, }; }, template: ` Down `, }); renderInWrapper(Host); await fireEvent.click(screen.getByTestId("thumbs-down-custom")); expect(customOnClick).toHaveBeenCalled(); }); }); describe("readAloudButton slot", () => { it("should call custom onClick on readAloudButton", async () => { const customOnClick = vi.fn(); const onReadAloud = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], customOnClick, onReadAloud }; }, template: ` Read `, }); renderInWrapper(Host); await fireEvent.click(screen.getByTestId("read-aloud-custom")); expect(customOnClick).toHaveBeenCalled(); }); }); describe("regenerateButton slot", () => { it("should call custom onClick on regenerateButton", async () => { const customOnClick = vi.fn(); const onRegenerate = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], customOnClick, onRegenerate, }; }, template: ` Regenerate `, }); renderInWrapper(Host); await fireEvent.click(screen.getByTestId("regenerate-custom")); expect(customOnClick).toHaveBeenCalled(); }); }); }); describe("3. Custom Component Receiving Sub-components", () => { it("should allow custom component for markdownRenderer", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("hello"); return { message, messages: [message] }; }, template: ` {{ content.toUpperCase() }} `, }); renderInWrapper(Host); const custom = screen.queryByTestId("custom-markdown-component"); expect(custom).toBeDefined(); expect(custom?.textContent).toBe("HELLO"); }); it("should allow custom component for toolbar", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello"); return { message, messages: [message], onThumbsUp: vi.fn() }; }, template: ` Custom Toolbar: `, }); renderInWrapper(Host); const custom = screen.queryByTestId("custom-toolbar-component"); expect(custom).toBeDefined(); expect(custom?.textContent).toContain("Custom Toolbar"); }); it("should allow custom component for copyButton", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello"); return { message, messages: [message] }; }, template: ` Custom Copy `, }); renderInWrapper(Host); expect(screen.queryByTestId("custom-copy")).toBeDefined(); }); }); describe("4. Children Render Function for Drill-down", () => { it("should provide all bound sub-components via children render function", () => { const onThumbsUp = vi.fn(); const onThumbsDown = vi.fn(); const onReadAloud = vi.fn(); const onRegenerate = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onThumbsUp, onThumbsDown, onReadAloud, onRegenerate, }; }, template: ` markdown toolbar copy up down read regen tools `, }); renderInWrapper(Host); expect(screen.queryByTestId("received-markdown")).toBeDefined(); expect(screen.queryByTestId("received-toolbar")).toBeDefined(); expect(screen.queryByTestId("received-copy")).toBeDefined(); expect(screen.queryByTestId("received-thumbs-up")).toBeDefined(); expect(screen.queryByTestId("received-thumbs-down")).toBeDefined(); expect(screen.queryByTestId("received-read-aloud")).toBeDefined(); expect(screen.queryByTestId("received-regenerate")).toBeDefined(); expect(screen.queryByTestId("received-tool-calls")).toBeDefined(); }); it("should pass message and other props through children render function", () => { const message = createAssistantMessage("Test message"); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { return { message, messages: [message] }; }, template: ` {{ receivedMessage.id }} `, }); renderInWrapper(Host); expect(screen.getByTestId("children-message").textContent).toBe("msg-1"); }); }); describe("5. className Override with Tailwind Strings", () => { it("should override root className while preserving default prose classes", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello"); return { message, messages: [message] }; }, template: ` `, }); const { container } = renderInWrapper(Host); const root = container.querySelector(".custom-root-class"); expect(root).toBeDefined(); const proseDiv = root?.classList.contains("cpk:prose") || !!root?.querySelector(".cpk\\:prose"); expect(proseDiv).toBe(true); }); it("should allow tailwind utilities to override default styles", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello"); return { message, messages: [message] }; }, template: ` `, }); const { container } = renderInWrapper(Host); expect(container.querySelector(".max-w-sm")).toBeDefined(); }); it("should merge multiple slot classNames correctly", () => { const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello"); return { message, messages: [message] }; }, template: ` Toolbar Copy `, }); const { container } = renderInWrapper(Host); expect(container.querySelector(".root-custom")).toBeDefined(); expect(container.querySelector(".toolbar-custom")).toBeDefined(); expect(container.querySelector(".copy-custom")).toBeDefined(); }); }); describe("6. Integration and Recursive Slot Application", () => { it("should correctly render all slots with mixed customization", () => { const onThumbsUp = vi.fn(); const onThumbsDown = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onThumbsUp, onThumbsDown }; }, template: ` {{ content }} Toolbar Copy Up Down `, }); const { container } = renderInWrapper(Host); expect(container.querySelector(".markdown-style")).toBeDefined(); expect(container.querySelector(".toolbar-style")).toBeDefined(); expect(container.querySelector(".copy-style")).toBeDefined(); expect(container.querySelector(".thumbs-up-style")).toBeDefined(); expect(container.querySelector(".thumbs-down-style")).toBeDefined(); }); it("should work with property objects and class strings mixed", async () => { const onClick = vi.fn(); const Host = defineComponent({ components: { CopilotChatAssistantMessage }, setup() { const message = createAssistantMessage("Hello world"); return { message, messages: [message], onClick, onThumbsUp: vi.fn() }; }, template: ` {{ content }} Toolbar `, }); const { container } = renderInWrapper(Host); expect(container.querySelector(".text-lg")).toBeDefined(); const toolbar = container.querySelector(".flex.gap-2"); if (toolbar) { await fireEvent.click(toolbar); expect(onClick).toHaveBeenCalled(); } }); }); });