diff --git a/src/components/ShareButtons.test.tsx b/src/components/ShareButtons.test.tsx index 2fe5b444..2725d3fa 100644 --- a/src/components/ShareButtons.test.tsx +++ b/src/components/ShareButtons.test.tsx @@ -8,13 +8,11 @@ import { logger } from "@/lib/logger"; describe("ShareButtons", () => { let originalClipboard: Navigator["clipboard"] | undefined; - let originalExecCommand: (commandId: string, showUI?: boolean, value?: string) => boolean; let originalLocation: Location; beforeEach(() => { vi.spyOn(logger, 'error').mockImplementation(() => {}); originalClipboard = navigator.clipboard; - originalExecCommand = document.execCommand; originalLocation = window.location; Object.defineProperty(window, "location", { @@ -30,7 +28,6 @@ describe("ShareButtons", () => { vi.useRealTimers(); vi.restoreAllMocks(); Object.assign(navigator, { clipboard: originalClipboard }); - document.execCommand = originalExecCommand; Object.defineProperty(window, "location", { value: originalLocation, @@ -38,61 +35,6 @@ describe("ShareButtons", () => { }); }); - it("uses document.execCommand as fallback when navigator.clipboard.writeText fails", async () => { - // 1. Mock clipboard.writeText to reject - const writeTextMock = vi.fn().mockRejectedValue(new Error("Not allowed")); - Object.assign(navigator, { - clipboard: { - writeText: writeTextMock, - }, - }); - - // 2. Mock execCommand - const execCommandMock = vi.fn().mockReturnValue(true); - document.execCommand = execCommandMock; - - // 3. Spy on document.createElement, document.body.appendChild, and document.body.removeChild - // to verify the full fallback flow - const createElementSpy = vi.spyOn(document, "createElement"); - const appendChildSpy = vi.spyOn(document.body, "appendChild"); - const removeChildSpy = vi.spyOn(document.body, "removeChild"); - - render(); - - const copyButton = screen.getByRole("button", { name: "Copy profile URL" }); - - fireEvent.click(copyButton); - - await waitFor(() => { - expect(writeTextMock).toHaveBeenCalledWith("http://localhost/johndoe"); - }); - - await waitFor(() => { - expect(createElementSpy).toHaveBeenCalledWith("textarea"); - - // Find the appendChild call that appends the textarea (since React might also call appendChild) - const textareaAppendCall = appendChildSpy.mock.calls.find( - (call) => (call[0] as HTMLElement).tagName === "TEXTAREA" - ); - - expect(textareaAppendCall).toBeDefined(); - if (textareaAppendCall) { - const appendedNode = textareaAppendCall[0] as HTMLTextAreaElement; - expect(appendedNode.value).toBe("http://localhost/johndoe"); - - expect(execCommandMock).toHaveBeenCalledWith("copy"); - - // Verify removeChild was called with the same element - expect(removeChildSpy).toHaveBeenCalledWith(appendedNode); - } - }); - - // Clear out React's state updates - await act(async () => { - vi.advanceTimersByTime(2500); - }); - }); - it("uses navigator.clipboard.writeText when available and successful", async () => { // Mock clipboard.writeText to succeed const writeTextMock = vi.fn().mockResolvedValue(undefined); @@ -102,9 +44,6 @@ describe("ShareButtons", () => { }, }); - const execCommandMock = vi.fn().mockReturnValue(true); - document.execCommand = execCommandMock; - render(); const copyButton = screen.getByRole("button", { name: "Copy profile URL" }); @@ -115,9 +54,6 @@ describe("ShareButtons", () => { expect(writeTextMock).toHaveBeenCalledWith("http://localhost/johndoe"); }); - // Fallback should not be triggered - expect(execCommandMock).not.toHaveBeenCalled(); - // Clear out React's state updates await act(async () => { vi.advanceTimersByTime(2500); @@ -155,7 +91,7 @@ describe("ShareButtons", () => { }); }); - it("logs an error and does not show 'Copied!' feedback when both copy methods fail", async () => { + it("logs an error and does not show 'Copied!' feedback when copy fails", async () => { // 1. Mock clipboard.writeText to reject const writeTextMock = vi.fn().mockRejectedValue(new Error("Clipboard API failed")); Object.assign(navigator, { @@ -164,10 +100,6 @@ describe("ShareButtons", () => { }, }); - // 2. Mock execCommand to return false (failure) - const execCommandMock = vi.fn().mockReturnValue(false); - document.execCommand = execCommandMock; - render(); const copyButton = screen.getByRole("button", { name: "Copy profile URL" }); @@ -178,15 +110,10 @@ describe("ShareButtons", () => { expect(writeTextMock).toHaveBeenCalledWith("http://localhost/johndoe"); }); - await waitFor(() => { - expect(execCommandMock).toHaveBeenCalledWith("copy"); - }); - // Verify logger.error was called expect(logger.error).toHaveBeenCalledWith( "Failed to copy", - expect.any(Error), // error from clipboard.writeText - expect.any(Error) // error from execCommand fallback failing + expect.any(Error) // error from clipboard.writeText ); // Verify button text remains unchanged @@ -209,13 +136,9 @@ describe("ShareButtons", () => { }; }; - it("uses document.execCommand as fallback when navigator.clipboard is undefined", async () => { + it("logs an error and does not show 'Copied!' feedback when clipboard is undefined", async () => { const restoreClipboard = setupUndefinedClipboard(); - // 2. Mock execCommand - const execCommandMock = vi.fn().mockReturnValue(true); - document.execCommand = execCommandMock; - render(); const copyButton = screen.getByRole("button", { name: "Copy profile URL" }); @@ -223,72 +146,13 @@ describe("ShareButtons", () => { fireEvent.click(copyButton); await waitFor(() => { - expect(execCommandMock).toHaveBeenCalledWith("copy"); - }); - - // Check for success feedback - await waitFor(() => { - expect(screen.getByText("Copied!")).toBeDefined(); - }); - - // Clear out React's state updates - await act(async () => { - vi.advanceTimersByTime(2500); - }); - - restoreClipboard(); - }); - - it("logs an error and does not show 'Copied!' feedback when both copy methods fail and clipboard is undefined", async () => { - const restoreClipboard = setupUndefinedClipboard(); - - const execCommandMock = vi.fn().mockReturnValue(false); - document.execCommand = execCommandMock; - - render(); - - const copyButton = screen.getByRole("button", { name: "Copy profile URL" }); - - fireEvent.click(copyButton); - - await waitFor(() => { - expect(execCommandMock).toHaveBeenCalledWith("copy"); - }); - - expect(logger.error).toHaveBeenCalledWith( - "Failed to copy", - expect.any(Error), - expect.any(Error) - ); - - restoreClipboard(); - }); - - it("uses document.execCommand as fallback and catches its error", async () => { - const restoreClipboard = setupUndefinedClipboard(); - - const execCommandMock = vi.fn().mockImplementation(() => { - throw new Error("execCommand crashed"); - }); - document.execCommand = execCommandMock; - - render(); - - const copyButton = screen.getByRole("button", { name: "Copy profile URL" }); - - fireEvent.click(copyButton); - - await waitFor(() => { - expect(execCommandMock).toHaveBeenCalledWith("copy"); + expect(logger.error).toHaveBeenCalledWith( + "Failed to copy", + expect.any(Error) + ); }); - expect(logger.error).toHaveBeenCalledWith( - "Failed to copy", - expect.any(Error), // clipboard API missing error - expect.any(Error) // execCommand crash error - ); - restoreClipboard(); }); -}); \ No newline at end of file +}); diff --git a/src/hooks/__tests__/useCopyToClipboard.test.ts b/src/hooks/__tests__/useCopyToClipboard.test.ts index c98dbc2e..47704a8e 100644 --- a/src/hooks/__tests__/useCopyToClipboard.test.ts +++ b/src/hooks/__tests__/useCopyToClipboard.test.ts @@ -13,14 +13,12 @@ vi.mock("@/lib/logger", () => ({ describe("useCopyToClipboard", () => { let originalClipboard: Clipboard | undefined; - let originalExecCommand: typeof document.execCommand; beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); originalClipboard = navigator.clipboard; - originalExecCommand = document.execCommand; // Mock clipboard Object.defineProperty(navigator, "clipboard", { @@ -29,9 +27,6 @@ describe("useCopyToClipboard", () => { }, configurable: true, }); - - // Mock document.execCommand - document.execCommand = vi.fn(); }); afterEach(() => { @@ -45,7 +40,6 @@ describe("useCopyToClipboard", () => { configurable: true, }); } - document.execCommand = originalExecCommand; vi.restoreAllMocks(); }); @@ -62,50 +56,6 @@ describe("useCopyToClipboard", () => { expect(navigator.clipboard.writeText).toHaveBeenCalledWith("test text"); expect(result.current.copied).toBe(true); - expect(document.execCommand).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(2000); - }); - - expect(result.current.copied).toBe(false); - }); - - it("should use fallback if navigator.clipboard is not available", async () => { - // Remove clipboard - // @ts-expect-error test setup - delete navigator.clipboard; - vi.mocked(document.execCommand).mockReturnValue(true); - - const { result } = renderHook(() => useCopyToClipboard()); - - await act(async () => { - await result.current.copyToClipboard("fallback text"); - }); - - expect(document.execCommand).toHaveBeenCalledWith("copy"); - expect(result.current.copied).toBe(true); - - act(() => { - vi.advanceTimersByTime(2000); - }); - - expect(result.current.copied).toBe(false); - }); - - it("should use fallback if navigator.clipboard.writeText fails", async () => { - vi.mocked(navigator.clipboard.writeText).mockRejectedValue(new Error("Clipboard error")); - vi.mocked(document.execCommand).mockReturnValue(true); - - const { result } = renderHook(() => useCopyToClipboard()); - - await act(async () => { - await result.current.copyToClipboard("fallback text"); - }); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith("fallback text"); - expect(document.execCommand).toHaveBeenCalledWith("copy"); - expect(result.current.copied).toBe(true); act(() => { vi.advanceTimersByTime(2000); @@ -114,33 +64,9 @@ describe("useCopyToClipboard", () => { expect(result.current.copied).toBe(false); }); - it("should log error if both clipboard and fallback fail", async () => { - vi.mocked(navigator.clipboard.writeText).mockRejectedValue(new Error("Clipboard error")); - vi.mocked(document.execCommand).mockReturnValue(false); - - const { result } = renderHook(() => useCopyToClipboard()); - - await act(async () => { - await result.current.copyToClipboard("failed text"); - }); - - expect(result.current.copied).toBe(false); - expect(logger.error).toHaveBeenCalledWith( - "Failed to copy", - expect.any(Error), - expect.any(Error) - ); - }); - - - it("should log error if both clipboard.writeText and fallback throw errors", async () => { - const writeError = new Error("Clipboard write error"); - const execError = new Error("execCommand thrown error"); - - vi.mocked(navigator.clipboard.writeText).mockRejectedValue(writeError); - vi.mocked(document.execCommand).mockImplementation(() => { - throw execError; - }); + it("should log error if navigator.clipboard.writeText fails", async () => { + const error = new Error("Clipboard error"); + vi.mocked(navigator.clipboard.writeText).mockRejectedValue(error); const { result } = renderHook(() => useCopyToClipboard()); @@ -148,20 +74,15 @@ describe("useCopyToClipboard", () => { await result.current.copyToClipboard("failed text"); }); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith("failed text"); expect(result.current.copied).toBe(false); - expect(logger.error).toHaveBeenCalledWith( - "Failed to copy", - writeError, - execError - ); + expect(logger.error).toHaveBeenCalledWith("Failed to copy", error); }); - it("should log error if fallback throws an error", async () => { + it("should log error if navigator.clipboard is not available", async () => { + // Remove clipboard // @ts-expect-error test setup delete navigator.clipboard; - vi.mocked(document.execCommand).mockImplementation(() => { - throw new Error("execCommand thrown error"); - }); const { result } = renderHook(() => useCopyToClipboard()); @@ -172,7 +93,6 @@ describe("useCopyToClipboard", () => { expect(result.current.copied).toBe(false); expect(logger.error).toHaveBeenCalledWith( "Failed to copy", - expect.any(Error), expect.any(Error) ); }); diff --git a/src/hooks/useCopyToClipboard.ts b/src/hooks/useCopyToClipboard.ts index 83d0f8e8..60cefbdc 100644 --- a/src/hooks/useCopyToClipboard.ts +++ b/src/hooks/useCopyToClipboard.ts @@ -18,49 +18,15 @@ export function useCopyToClipboard(timeout = 2000) { }, [timeout]); const copyToClipboard = useCallback(async (text: string) => { - let clipboardError: unknown = null; - if (navigator.clipboard && navigator.clipboard.writeText) { try { await navigator.clipboard.writeText(text); showCopiedFeedback(); - return; } catch (err) { - clipboardError = err; + logger.error("Failed to copy", err); } } else { - clipboardError = new Error("Clipboard API not available"); - } - - // Fallback for older browsers - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.position = "fixed"; - textArea.style.left = "-9999px"; - textArea.style.top = "0"; - textArea.setAttribute("readonly", ""); - document.body.appendChild(textArea); - - let successful = false; - let fallbackError: unknown = null; - - try { - textArea.select(); - successful = document.execCommand("copy"); - if (!successful) { - fallbackError = new Error("document.execCommand('copy') failed"); - } - } catch (err) { - successful = false; - fallbackError = err; - } finally { - document.body.removeChild(textArea); - } - - if (successful) { - showCopiedFeedback(); - } else { - logger.error("Failed to copy", clipboardError, fallbackError); + logger.error("Failed to copy", new Error("Clipboard API not available")); } }, [showCopiedFeedback]);