From 8a1503ef749bc6002e285022020211962217488a Mon Sep 17 00:00:00 2001 From: Usman Abeeb <136495186+therealbibson@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:34:19 +0100 Subject: [PATCH] fix(components): add copy success animation and aria-live feedback to CopyLinkButton Closes #663 --- src/__tests__/CopyLinkButton.test.tsx | 65 +++++++++++++++++++++++++++ src/components/CopyLinkButton.tsx | 56 ++++++++++++++++++++--- 2 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/__tests__/CopyLinkButton.test.tsx diff --git a/src/__tests__/CopyLinkButton.test.tsx b/src/__tests__/CopyLinkButton.test.tsx new file mode 100644 index 00000000..4e310e15 --- /dev/null +++ b/src/__tests__/CopyLinkButton.test.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import CopyLinkButton from "@/components/CopyLinkButton"; + +const URL = "https://stellarsplit.app/i/42"; + +function mockClipboard() { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + value: { writeText }, + configurable: true, + writable: true, + }); + return writeText; +} + +describe("CopyLinkButton", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("starts in the idle state", () => { + mockClipboard(); + render(); + + expect( + screen.getByRole("button", { name: /copy verification link/i }) + ).toBeInTheDocument(); + expect(screen.getByText("Copy Link")).toBeInTheDocument(); + }); + + it("switches to the success state and announces after a successful copy", async () => { + const writeText = mockClipboard(); + render(); + + fireEvent.click( + screen.getByRole("button", { name: /copy verification link/i }) + ); + + await waitFor(() => + expect(screen.getByText("Copied!")).toBeInTheDocument() + ); + + expect(writeText).toHaveBeenCalledWith(URL); + + // Icon swap: the checkmark replaces the link icon. + const button = screen.getByRole("button", { name: /copied/i }); + expect(button.querySelector("svg")).toBeTruthy(); + + // Green success styling for the 2s window. + expect(button.className).toContain("bg-green-600"); + + // Screen-reader announcement. + expect(screen.getByText("Link copied")).toBeInTheDocument(); + }); + + it("exposes a polite live region for the announcement", () => { + mockClipboard(); + const { container } = render(); + + const live = container.querySelector('[aria-live="polite"]'); + expect(live).not.toBeNull(); + expect(live).toHaveTextContent(""); + }); +}); diff --git a/src/components/CopyLinkButton.tsx b/src/components/CopyLinkButton.tsx index e6c2c684..a7f3c82e 100644 --- a/src/components/CopyLinkButton.tsx +++ b/src/components/CopyLinkButton.tsx @@ -6,12 +6,54 @@ export default function CopyLinkButton({ url }: { url: string }) { const { copy, copied } = useCopyToClipboard(); return ( - + <> + + + + {copied ? "Link copied" : ""} + + ); }