import React from "react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { CopilotChatAssistantMessage } from "../CopilotChatAssistantMessage"; import { CopilotChatUserMessage } from "../CopilotChatUserMessage"; import { CopilotKitProvider } from "../../../providers/CopilotKitProvider"; import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider"; import { AssistantMessage, UserMessage } from "@ag-ui/core"; const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); const createAssistantMessage = (content: string): AssistantMessage => ({ id: "msg-assistant-1", role: "assistant", content, }); const createUserMessage = (content: string): UserMessage => ({ id: "msg-user-1", role: "user", content, }); describe("CopyButton clipboard behavior", () => { let originalClipboard: Clipboard; beforeEach(() => { originalClipboard = navigator.clipboard; }); afterEach(() => { Object.defineProperty(navigator, "clipboard", { value: originalClipboard, writable: true, configurable: true, }); }); describe("AssistantMessage CopyButton", () => { it("shows copied state only after successful clipboard write", async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { value: { writeText: writeTextMock }, writable: true, configurable: true, }); const message = createAssistantMessage("Hello assistant"); render( , ); const copyButton = screen.getByTestId("copilot-copy-button"); fireEvent.click(copyButton); await waitFor(() => { expect(writeTextMock).toHaveBeenCalledWith("Hello assistant"); }); // After successful write, should show check icon await waitFor(() => { const checkIcon = copyButton.querySelector(".lucide-check"); expect(checkIcon).not.toBeNull(); }); }); it("does NOT show copied state when clipboard API is unavailable", async () => { Object.defineProperty(navigator, "clipboard", { value: undefined, writable: true, configurable: true, }); const message = createAssistantMessage("Hello assistant"); render( , ); const copyButton = screen.getByTestId("copilot-copy-button"); fireEvent.click(copyButton); // Wait a tick for any async handlers await new Promise((r) => setTimeout(r, 50)); // Should still show copy icon (not check icon) const checkIcon = copyButton.querySelector(".lucide-check"); expect(checkIcon).toBeNull(); const copyIcon = copyButton.querySelector(".lucide-copy"); expect(copyIcon).not.toBeNull(); }); it("logs error when clipboard write rejects", async () => { const writeTextMock = vi .fn() .mockRejectedValue(new Error("Permission denied")); Object.defineProperty(navigator, "clipboard", { value: { writeText: writeTextMock }, writable: true, configurable: true, }); const consoleSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); const message = createAssistantMessage("Hello assistant"); render( , ); const copyButton = screen.getByTestId("copilot-copy-button"); fireEvent.click(copyButton); await waitFor(() => { expect(writeTextMock).toHaveBeenCalled(); }); await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( "Failed to copy to clipboard:", expect.any(Error), ); }); // Should NOT show copied state when write failed const checkIcon = copyButton.querySelector(".lucide-check"); expect(checkIcon).toBeNull(); consoleSpy.mockRestore(); }); }); describe("UserMessage CopyButton", () => { it("shows copied state only after successful clipboard write", async () => { const writeTextMock = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { value: { writeText: writeTextMock }, writable: true, configurable: true, }); const message = createUserMessage("Hello user"); render( , ); const copyButton = screen.getByTestId("copilot-user-copy-button"); fireEvent.click(copyButton); await waitFor(() => { expect(writeTextMock).toHaveBeenCalledWith("Hello user"); }); await waitFor(() => { const checkIcon = copyButton.querySelector(".lucide-check"); expect(checkIcon).not.toBeNull(); }); }); it("does NOT show copied state when clipboard API is unavailable", async () => { Object.defineProperty(navigator, "clipboard", { value: undefined, writable: true, configurable: true, }); const message = createUserMessage("Hello user"); render( , ); const copyButton = screen.getByTestId("copilot-user-copy-button"); fireEvent.click(copyButton); await new Promise((r) => setTimeout(r, 50)); const checkIcon = copyButton.querySelector(".lucide-check"); expect(checkIcon).toBeNull(); const copyIcon = copyButton.querySelector(".lucide-copy"); expect(copyIcon).not.toBeNull(); }); it("logs error when clipboard write rejects", async () => { const writeTextMock = vi .fn() .mockRejectedValue(new Error("Permission denied")); Object.defineProperty(navigator, "clipboard", { value: { writeText: writeTextMock }, writable: true, configurable: true, }); const consoleSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); const message = createUserMessage("Hello user"); render( , ); const copyButton = screen.getByTestId("copilot-user-copy-button"); fireEvent.click(copyButton); await waitFor(() => { expect(writeTextMock).toHaveBeenCalled(); }); await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith( "Failed to copy to clipboard:", expect.any(Error), ); }); // Should NOT show copied state when write failed const checkIcon = copyButton.querySelector(".lucide-check"); expect(checkIcon).toBeNull(); consoleSpy.mockRestore(); }); }); });