From f96cc82dc6be4239ae12cc6f59154402f69d2120 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 21 Jul 2025 22:54:04 +0000 Subject: [PATCH] fix: disable browser context menu in Roo Code chat interface - Add onContextMenu handler to ChatView component to prevent default - Add onContextMenu handler to ChatRow component to prevent default - Add onContextMenu handler to ChatTextArea component to prevent default - Add tests to verify context menu prevention works correctly Fixes #6038 --- webview-ui/src/components/chat/ChatRow.tsx | 7 +- .../src/components/chat/ChatTextArea.tsx | 8 +- webview-ui/src/components/chat/ChatView.tsx | 9 +- .../__tests__/ChatView.context-menu.spec.tsx | 196 ++++++++++++++++++ 4 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 926bd400f0..b01f4fd20e 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -72,8 +72,13 @@ const ChatRow = memo( // This allows us to detect changes without causing re-renders const prevHeightRef = useRef(0) + // Prevent browser context menu from appearing + const handleContextMenu = useCallback((e: React.MouseEvent) => { + e.preventDefault() + }, []) + const [chatrow, { height }] = useSize( -
+
, ) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6c541353eb..0c35a929b0 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1174,6 +1174,11 @@ const ChatTextArea = forwardRef(
) + // Prevent browser context menu from appearing + const handleContextMenu = useCallback((e: React.MouseEvent) => { + e.preventDefault() + }, []) + return (
( "ml-auto", "mr-auto", "box-border", - )}> + )} + onContextMenu={handleContextMenu}>
{ + e.preventDefault() + }, []) + return ( -
+
{(showAnnouncement || showAnnouncementModal) && ( { diff --git a/webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx new file mode 100644 index 0000000000..e657f11488 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx @@ -0,0 +1,196 @@ +// npx vitest run src/components/chat/__tests__/ChatView.context-menu.spec.tsx + +import React from "react" +import { render, fireEvent } from "@testing-library/react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { vi } from "vitest" + +import ChatView, { ChatViewProps } from "../ChatView" + +// Mock the ExtensionStateContext +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + didHydrateState: true, + showWelcome: false, + shouldShowAnnouncement: false, + clineMessages: [], + currentTaskItem: null, + taskHistory: [], + apiConfiguration: null, + organizationAllowList: [], + mcpServers: [], + alwaysAllowBrowser: false, + alwaysAllowReadOnly: false, + alwaysAllowReadOnlyOutsideWorkspace: false, + alwaysAllowWrite: false, + alwaysAllowWriteOutsideWorkspace: false, + alwaysAllowWriteProtected: false, + alwaysAllowExecute: false, + alwaysAllowMcp: false, + allowedCommands: [], + deniedCommands: [], + writeDelayMs: 0, + followupAutoApproveTimeoutMs: 0, + mode: "code", + setMode: vi.fn(), + autoApprovalEnabled: false, + alwaysAllowModeSwitch: false, + alwaysAllowSubtasks: false, + alwaysAllowFollowupQuestions: false, + alwaysAllowUpdateTodoList: false, + customModes: [], + telemetrySetting: "unset", + hasSystemPromptOverride: false, + historyPreviewCollapsed: false, + soundEnabled: false, + soundVolume: 0.5, + }), +})) + +// Mock other dependencies +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("../ChatRow", () => ({ + default: function MockChatRow({ message }: { message: any }) { + return
{JSON.stringify(message)}
+ }, +})) + +vi.mock("../ChatTextArea", () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const React = require("react") + return { + default: React.forwardRef(function MockChatTextArea( + _props: any, + ref: React.ForwardedRef<{ focus: () => void }>, + ) { + React.useImperativeHandle(ref, () => ({ + focus: vi.fn(), + })) + return
Chat Text Area
+ }), + } +}) + +vi.mock("../TaskHeader", () => ({ + default: function MockTaskHeader() { + return
Task Header
+ }, +})) + +vi.mock("../AutoApproveMenu", () => ({ + default: function MockAutoApproveMenu() { + return
Auto Approve Menu
+ }, +})) + +vi.mock("@src/components/welcome/RooHero", () => ({ + default: function MockRooHero() { + return
Roo Hero
+ }, +})) + +vi.mock("@src/components/welcome/RooTips", () => ({ + default: function MockRooTips() { + return
Roo Tips
+ }, +})) + +vi.mock("../history/HistoryPreview", () => ({ + default: function MockHistoryPreview() { + return
History Preview
+ }, +})) + +vi.mock("@src/components/common/TelemetryBanner", () => ({ + default: function MockTelemetryBanner() { + return
Telemetry Banner
+ }, +})) + +vi.mock("@src/components/common/VersionIndicator", () => ({ + default: function MockVersionIndicator() { + return
Version Indicator
+ }, +})) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, +}) + +const defaultProps: ChatViewProps = { + isHidden: false, + showAnnouncement: false, + hideAnnouncement: vi.fn(), +} + +const renderChatView = (props: Partial = {}) => { + return render( + + + , + ) +} + +describe("ChatView - Context Menu Prevention", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should prevent default context menu on right-click", () => { + const { container } = renderChatView() + + // Find the main chat view container + const chatViewContainer = container.querySelector(".fixed") + expect(chatViewContainer).toBeTruthy() + + // Create a context menu event + const contextMenuEvent = new MouseEvent("contextmenu", { + bubbles: true, + cancelable: true, + button: 2, // Right mouse button + }) + + // Spy on preventDefault + const preventDefaultSpy = vi.spyOn(contextMenuEvent, "preventDefault") + + // Fire the context menu event + if (chatViewContainer) { + fireEvent(chatViewContainer, contextMenuEvent) + } + + // Verify preventDefault was called + expect(preventDefaultSpy).toHaveBeenCalled() + }) + + it("should prevent context menu on nested elements", () => { + const { getByTestId } = renderChatView() + + // Test on a nested element (e.g., chat text area) + const chatTextArea = getByTestId("chat-text-area") + + const contextMenuEvent = new MouseEvent("contextmenu", { + bubbles: true, + cancelable: true, + button: 2, + }) + + const preventDefaultSpy = vi.spyOn(contextMenuEvent, "preventDefault") + + fireEvent(chatTextArea, contextMenuEvent) + + // The event should bubble up and be prevented at the parent level + expect(preventDefaultSpy).toHaveBeenCalled() + }) +})