From fbad0b462a1ff6d42bb3f59cd6349ac515fbc5c6 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 05:54:49 -0500 Subject: [PATCH 1/2] fix(web): failed message and diff path copies show an error toast Use the existing error notification so failed message and diff-path copies show their clipboard error message and the normal dismissal time. Co-Authored-By: GPT-6.1 Sol --- .../chat/MessageCopyButton.browser.test.tsx | 30 +++++++++++++++++++ .../src/components/ui/anchoredCopyToast.ts | 5 +--- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/chat/MessageCopyButton.browser.test.tsx diff --git a/apps/web/src/components/chat/MessageCopyButton.browser.test.tsx b/apps/web/src/components/chat/MessageCopyButton.browser.test.tsx new file mode 100644 index 0000000000000..fdae9c7afcacf --- /dev/null +++ b/apps/web/src/components/chat/MessageCopyButton.browser.test.tsx @@ -0,0 +1,30 @@ +import { expect, it, vi } from "vite-plus/test"; +import { page } from "vite-plus/test/browser"; + +import { render } from "../../../test/render"; +import { AnchoredToastProvider } from "../ui/toast"; +import { MessageCopyButton } from "./MessageCopyButton"; + +it("explains a denied clipboard write and lets the same message be copied again", async () => { + await using screen = await render( + + + , + ); + using _write = vi + .spyOn(navigator.clipboard, "writeText") + .mockRejectedValueOnce(new DOMException("Clipboard access denied", "NotAllowedError")) + .mockResolvedValue(undefined); + using _console = vi.spyOn(console, "error").mockImplementation(() => undefined); + + const copy = screen.getByRole("button", { name: "Copy message" }); + await copy.click(); + + await expect.element(page.getByText("Failed to copy text to the clipboard.")).toBeVisible(); + await expect.element(copy).toBeEnabled(); + await page.getByRole("button", { name: "Dismiss notification" }).click(); + await copy.click(); + + await expect.element(page.getByText("Copied!", { exact: true })).toBeVisible(); + await expect.element(copy).toBeDisabled(); +}); diff --git a/apps/web/src/components/ui/anchoredCopyToast.ts b/apps/web/src/components/ui/anchoredCopyToast.ts index df1ac579c89cf..a22d3104f9ef6 100644 --- a/apps/web/src/components/ui/anchoredCopyToast.ts +++ b/apps/web/src/components/ui/anchoredCopyToast.ts @@ -20,13 +20,10 @@ export function showAnchoredCopySuccessToast(ref: RefObject, error: Error) { if (!ref.current) return; anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, + type: "error", positionerProps: { anchor: ref.current, }, - timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, title: "Failed to copy", description: error.message, }); From 3bd0ee5c7e827d5d7934e1474d6d4bbcde545e7a Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Fri, 23 Oct 2026 21:09:29 -0500 Subject: [PATCH 2/2] test(web): cover denied diff path copy and retry DiffFilePathCopyButton has its own clipboard hook call, so the message button test does not cover it. Check that a denied write shows the error, dismissal works, and a second copy of the same path succeeds. Co-Authored-By: Claude Opus 5.5 --- .../DiffFilePathCopyButton.browser.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 apps/web/src/components/DiffFilePathCopyButton.browser.test.tsx diff --git a/apps/web/src/components/DiffFilePathCopyButton.browser.test.tsx b/apps/web/src/components/DiffFilePathCopyButton.browser.test.tsx new file mode 100644 index 0000000000000..a2bec09ca960d --- /dev/null +++ b/apps/web/src/components/DiffFilePathCopyButton.browser.test.tsx @@ -0,0 +1,30 @@ +import { expect, it, vi } from "vite-plus/test"; +import { page } from "vite-plus/test/browser"; + +import { render } from "../../test/render"; +import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton"; +import { AnchoredToastProvider } from "./ui/toast"; + +it("explains a denied clipboard write and lets the same path be copied again", async () => { + await using screen = await render( + + + , + ); + using write = vi + .spyOn(navigator.clipboard, "writeText") + .mockRejectedValueOnce(new DOMException("Clipboard access denied", "NotAllowedError")) + .mockResolvedValue(undefined); + using _console = vi.spyOn(console, "error").mockImplementation(() => undefined); + + const copy = screen.getByRole("button", { name: "Copy file path" }); + await copy.click(); + + await expect.element(page.getByText("Failed to copy text to the clipboard.")).toBeVisible(); + await page.getByRole("button", { name: "Dismiss notification" }).click(); + await copy.click(); + + await expect.element(page.getByText("Copied!", { exact: true })).toBeVisible(); + expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenLastCalledWith("apps/web/src/index.ts"); +});