diff --git a/app/(tools)/color-converter/ColorConverterClient.test.tsx b/app/(tools)/color-converter/ColorConverterClient.test.tsx index 1f6e864..ac45828 100644 --- a/app/(tools)/color-converter/ColorConverterClient.test.tsx +++ b/app/(tools)/color-converter/ColorConverterClient.test.tsx @@ -2,11 +2,45 @@ import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ColorConverterClient } from "./ColorConverterClient"; import { contrastRatio, parseColor } from "@/lib/color-utils"; +import { useRegisterCommands } from "@/components/shared/CommandPalette"; + +const { push } = vi.hoisted(() => ({ push: vi.fn() })); vi.mock("@/components/shared/CommandPalette", () => ({ useRegisterCommands: vi.fn() })); -vi.mock("@/components/shared/Toast", () => ({ useToast: () => ({ push: vi.fn() }) })); +vi.mock("@/components/shared/Toast", () => ({ useToast: () => ({ push }) })); afterEach(cleanup); +describe("palette clipboard feedback", () => { + it.each([ + ["hex", "#ff0000"], + ["rgb", "rgb(255, 0, 0)"], + ["hsl", "hsl(0, 100%, 50%)"], + ])("reports %s copy results with the correct value", async (format, expected) => { + push.mockClear(); + const writeText = vi.fn().mockRejectedValue(new Error("Clipboard denied")); + const clipboard = Object.getOwnPropertyDescriptor(navigator, "clipboard"); + Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); + try { + render(); + fireEvent.change(screen.getByLabelText("Color input"), { target: { value: "#ff0000" } }); + const commands = vi.mocked(useRegisterCommands).mock.calls.at(-1)![1]; + const command = commands.find(({ id }) => id === `color:copy-${format}`)!; + await expect(command.run()).resolves.toBeUndefined(); + expect(writeText).toHaveBeenCalledOnce(); + expect(writeText).toHaveBeenCalledWith(expected); + expect(push).toHaveBeenCalledExactlyOnceWith("Could not copy — clipboard blocked", "error"); + push.mockClear(); + writeText.mockReset().mockResolvedValue(undefined); + await command.run(); + expect(writeText).toHaveBeenCalledExactlyOnceWith(expected); + expect(push).toHaveBeenCalledExactlyOnceWith(`Copied ${format.toUpperCase()} to clipboard`, "success"); + } finally { + if (clipboard) Object.defineProperty(navigator, "clipboard", clipboard); + else Reflect.deleteProperty(navigator, "clipboard"); + } + }); +}); + function failingPair() { render(); const foreground = screen.getByLabelText("Text / Foreground"); diff --git a/app/(tools)/color-converter/ColorConverterClient.tsx b/app/(tools)/color-converter/ColorConverterClient.tsx index 086db50..d2cca7d 100644 --- a/app/(tools)/color-converter/ColorConverterClient.tsx +++ b/app/(tools)/color-converter/ColorConverterClient.tsx @@ -162,8 +162,12 @@ export function ColorConverterClient() { title: "Copy current color as HEX", run: async () => { if (hexValue) { - await navigator.clipboard.writeText(hexValue); - push("Copied HEX to clipboard", "success"); + try { + await navigator.clipboard.writeText(hexValue); + push("Copied HEX to clipboard", "success"); + } catch { + push("Could not copy — clipboard blocked", "error"); + } } }, }, @@ -173,8 +177,12 @@ export function ColorConverterClient() { title: "Copy current color as RGB", run: async () => { if (rgbValue) { - await navigator.clipboard.writeText(rgbValue); - push("Copied RGB to clipboard", "success"); + try { + await navigator.clipboard.writeText(rgbValue); + push("Copied RGB to clipboard", "success"); + } catch { + push("Could not copy — clipboard blocked", "error"); + } } }, }, @@ -184,8 +192,12 @@ export function ColorConverterClient() { title: "Copy current color as HSL", run: async () => { if (hslValue) { - await navigator.clipboard.writeText(hslValue); - push("Copied HSL to clipboard", "success"); + try { + await navigator.clipboard.writeText(hslValue); + push("Copied HSL to clipboard", "success"); + } catch { + push("Could not copy — clipboard blocked", "error"); + } } }, },