import "@testing-library/jest-dom"; import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { getWrapper } from "../../testUtils"; import { CopyButton } from "./CopyButton.component"; describe("CopyButton", () => { const { Wrapper } = getWrapper(); const originalClipboard = navigator.clipboard; beforeEach(() => { Object.assign(navigator, { clipboard: { writeText: jest.fn(), write: jest.fn(), }, }); // JSDOM doesn't define ClipboardItem; needed for copyAsHtml path. Stub only — full interface not needed for tests. if (typeof globalThis.ClipboardItem === "undefined") { // @ts-expect-error — intentional stub for JSDOM; global ClipboardItem type expects full interface (getType, types, etc.) globalThis.ClipboardItem = class {}; } }); afterEach(() => { Object.assign(navigator, { clipboard: originalClipboard }); jest.restoreAllMocks(); }); it("when writeText returns a rejected Promise, should enter the unsupported state", async () => { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockRejectedValueOnce(new Error("Clipboard denied")); render(, { wrapper: Wrapper }); const button = screen.getByRole("button", { name: "Copy" }); await act(() => userEvent.click(button)); await waitFor(() => { expect(writeTextMock).toHaveBeenCalledWith("test"); }); // Unsupported state: button should not show success, and not be aria-disabled expect( screen.queryByRole("button", { name: "Copied !" }), ).not.toBeInTheDocument(); const buttonAfterReject = screen.getByRole("button", { name: "Copy" }); expect(buttonAfterReject).not.toHaveAttribute("aria-disabled", "true"); }); it("during success state click does nothing; after 2s resets to idle and click works again", async () => { jest.useFakeTimers(); try { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockResolvedValue(undefined); render(, { wrapper: Wrapper }); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect( screen.getByRole("button", { name: "Copied !" }), ).toBeInTheDocument(); }); expect(writeTextMock).toHaveBeenCalledTimes(1); const inSuccess = screen.getByRole("button", { name: "Copied !" }); expect(inSuccess).toHaveAttribute("aria-disabled", "true"); await act(() => userEvent.click(inSuccess)); expect(writeTextMock).toHaveBeenCalledTimes(1); act(() => { jest.advanceTimersByTime(2000); }); await waitFor(() => { expect( screen.getByRole("button", { name: "Copy" }), ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Copy" }), ).not.toHaveAttribute("aria-disabled", "true"); }); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect(writeTextMock).toHaveBeenCalledTimes(2); }); } finally { jest.useRealTimers(); } }); it("standard copy (writeText): on success shows Copied ! tooltip and aria-disabled", async () => { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockResolvedValueOnce(undefined); render(, { wrapper: Wrapper }); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect(writeTextMock).toHaveBeenCalledWith("hello"); const button = screen.getByRole("button", { name: "Copied !" }); expect(button).toHaveAttribute("aria-disabled", "true"); }); }); it("copyAsHtml: calls clipboard.write and on success shows Copied ! and aria-disabled", async () => { const writeMock = navigator.clipboard.write as jest.Mock; writeMock.mockResolvedValueOnce(undefined); render(, { wrapper: Wrapper, }); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect(writeMock).toHaveBeenCalledTimes(1); expect(writeMock.mock.calls[0][0]).toHaveLength(1); expect( screen.getByRole("button", { name: "Copied !" }), ).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Copied !" })).toHaveAttribute( "aria-disabled", "true", ); }); }); it("copyAsHtml: when write rejects, enters unsupported state", async () => { const writeMock = navigator.clipboard.write as jest.Mock; writeMock.mockRejectedValueOnce(new Error("Denied")); render(, { wrapper: Wrapper, }); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect(writeMock).toHaveBeenCalled(); }); expect( screen.queryByRole("button", { name: "Copied !" }), ).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "Copy" })).not.toHaveAttribute( "aria-disabled", "true", ); }); it("ghost variant without label: tooltip is Copy then Copied ! on success", async () => { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockResolvedValueOnce(undefined); render(, { wrapper: Wrapper }); expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect( screen.getByRole("button", { name: "Copied !" }), ).toBeInTheDocument(); }); }); it("ghost variant with label: tooltip is Copy {label} then Copied ! on success", async () => { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockResolvedValueOnce(undefined); render(, { wrapper: Wrapper }); expect( screen.getByRole("button", { name: "Copy key" }), ).toBeInTheDocument(); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy key" })), ); await waitFor(() => { expect( screen.getByRole("button", { name: "Copied !" }), ).toBeInTheDocument(); }); }); it("outline variant without label: tooltip is Copy then Copied! on success", async () => { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockResolvedValueOnce(undefined); render(, { wrapper: Wrapper, }); expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy" })), ); await waitFor(() => { expect( screen.getByRole("button", { name: "Copied!" }), ).toBeInTheDocument(); }); }); it("outline variant with label: tooltip is Copy {label} then Copied {label}! on success", async () => { const writeTextMock = navigator.clipboard.writeText as jest.Mock; writeTextMock.mockResolvedValueOnce(undefined); render(, { wrapper: Wrapper, }); expect( screen.getByRole("button", { name: "Copy key" }), ).toBeInTheDocument(); await act(() => userEvent.click(screen.getByRole("button", { name: "Copy key" })), ); await waitFor(() => { expect( screen.getByRole("button", { name: "Copied key!" }), ).toBeInTheDocument(); }); }); });