From 123384975ee5c6b32f217fef85e3b053c5adee3d Mon Sep 17 00:00:00 2001 From: chengyongru Date: Thu, 25 Jun 2026 13:37:55 +0800 Subject: [PATCH] fix(webui): restore code block copy fallback --- webui/src/components/CodeBlock.tsx | 5 +++-- webui/src/tests/code-block.test.tsx | 31 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/webui/src/components/CodeBlock.tsx b/webui/src/components/CodeBlock.tsx index 5fd1c51a..5f8ed376 100644 --- a/webui/src/components/CodeBlock.tsx +++ b/webui/src/components/CodeBlock.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { useThemeValue } from "@/hooks/useTheme"; import { hasAnsi, parseAnsiSegments, stripAnsi } from "@/lib/ansi"; +import { copyTextToClipboard } from "@/lib/clipboard"; import { cn } from "@/lib/utils"; interface CodeBlockProps { @@ -192,8 +193,8 @@ export function CodeBlock({ const renderAnsi = shouldRenderAnsi(language, code); const onCopy = useCallback(() => { - if (!navigator.clipboard) return; - navigator.clipboard.writeText(renderAnsi ? stripAnsi(code) : code).then(() => { + void copyTextToClipboard(renderAnsi ? stripAnsi(code) : code).then((ok) => { + if (!ok) return; setCopied(true); setTimeout(() => setCopied(false), 1_500); }); diff --git a/webui/src/tests/code-block.test.tsx b/webui/src/tests/code-block.test.tsx index f8bafcea..0b4b4b22 100644 --- a/webui/src/tests/code-block.test.tsx +++ b/webui/src/tests/code-block.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; @@ -146,6 +146,35 @@ describe("CodeBlock", () => { } }); + it("copies with the textarea fallback when Clipboard API is unavailable", async () => { + const user = userEvent.setup(); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: undefined, + }); + const execCommand = vi.fn().mockReturnValue(true); + Object.defineProperty(document, "execCommand", { + configurable: true, + value: execCommand, + }); + + try { + render( + + + , + ); + + await user.click(screen.getByRole("button", { name: /copy/i })); + + await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy")); + expect(screen.getByText("Copied")).toBeInTheDocument(); + } finally { + Reflect.deleteProperty(navigator, "clipboard"); + Reflect.deleteProperty(document, "execCommand"); + } + }); + it("reads theme from context without creating per-block observers", async () => { const originalMutationObserver = globalThis.MutationObserver; const observer = vi.fn();