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]);