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");
+ }
}
},
},