mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
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
This commit is contained in:
parent
2eb586b422
commit
f96cc82dc6
4 changed files with 217 additions and 3 deletions
|
|
@ -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(
|
||||
<div className="px-[15px] py-[10px] pr-[6px]">
|
||||
<div className="px-[15px] py-[10px] pr-[6px]" onContextMenu={handleContextMenu}>
|
||||
<ChatRowContent {...props} />
|
||||
</div>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1174,6 +1174,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
</div>
|
||||
)
|
||||
|
||||
// Prevent browser context menu from appearing
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -1191,7 +1196,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
"ml-auto",
|
||||
"mr-auto",
|
||||
"box-border",
|
||||
)}>
|
||||
)}
|
||||
onContextMenu={handleContextMenu}>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")}
|
||||
|
|
|
|||
|
|
@ -1629,8 +1629,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
|
||||
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
|
||||
|
||||
// Prevent browser context menu from appearing
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
|
||||
<div
|
||||
className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}
|
||||
onContextMenu={handleContextMenu}>
|
||||
{(showAnnouncement || showAnnouncementModal) && (
|
||||
<Announcement
|
||||
hideAnnouncement={() => {
|
||||
|
|
|
|||
|
|
@ -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 <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
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 <div data-testid="chat-text-area">Chat Text Area</div>
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("../TaskHeader", () => ({
|
||||
default: function MockTaskHeader() {
|
||||
return <div data-testid="task-header">Task Header</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../AutoApproveMenu", () => ({
|
||||
default: function MockAutoApproveMenu() {
|
||||
return <div data-testid="auto-approve-menu">Auto Approve Menu</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/welcome/RooHero", () => ({
|
||||
default: function MockRooHero() {
|
||||
return <div data-testid="roo-hero">Roo Hero</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/welcome/RooTips", () => ({
|
||||
default: function MockRooTips() {
|
||||
return <div data-testid="roo-tips">Roo Tips</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../history/HistoryPreview", () => ({
|
||||
default: function MockHistoryPreview() {
|
||||
return <div data-testid="history-preview">History Preview</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/common/TelemetryBanner", () => ({
|
||||
default: function MockTelemetryBanner() {
|
||||
return <div data-testid="telemetry-banner">Telemetry Banner</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/components/common/VersionIndicator", () => ({
|
||||
default: function MockVersionIndicator() {
|
||||
return <div data-testid="version-indicator">Version Indicator</div>
|
||||
},
|
||||
}))
|
||||
|
||||
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<ChatViewProps> = {}) => {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatView {...defaultProps} {...props} />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue