diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 6b33a9b7f7..332ef18511 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -20,6 +20,8 @@ import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" +import { TooltipProvider } from "./components/ui/tooltip" +import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" @@ -215,7 +217,9 @@ const AppWithProviders = () => ( - + + + diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index d4e8b26818..2c55d1cf07 100644 --- a/webview-ui/src/__tests__/App.spec.tsx +++ b/webview-ui/src/__tests__/App.spec.tsx @@ -1,7 +1,7 @@ // npx vitest run src/__tests__/App.spec.tsx import React from "react" -import { render, screen, act, cleanup } from "@testing-library/react" +import { render, screen, act, cleanup } from "@/utils/test-utils" import AppWithProviders from "../App" diff --git a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx index 5a5ff463ef..4220fd1d3a 100644 --- a/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx +++ b/webview-ui/src/__tests__/ContextWindowProgress.spec.tsx @@ -1,6 +1,6 @@ // npm run test ContextWindowProgress.spec.tsx -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import TaskHeader from "@src/components/chat/TaskHeader" @@ -103,18 +103,22 @@ describe("ContextWindowProgress", () => { it("calculates percentages correctly", () => { renderComponent({ contextTokens: 1000, contextWindow: 4000 }) - // Instead of checking the title attribute, verify the data-test-id - // which identifies the element containing info about the percentage of tokens used - const tokenUsageDiv = screen.getByTestId("context-tokens-used") - expect(tokenUsageDiv).toBeInTheDocument() + // Verify that the token count and window size are displayed correctly + const tokenCount = screen.getByTestId("context-tokens-count") + const windowSize = screen.getByTestId("context-window-size") - // Just verify that the element has a title attribute (the actual text is translated and may vary) - expect(tokenUsageDiv).toHaveAttribute("title") + expect(tokenCount).toBeInTheDocument() + expect(tokenCount).toHaveTextContent("1000") - // We can't reliably test computed styles in JSDOM, so we'll just check - // that the component appears to be working correctly by checking for expected elements - // The context-window-label is not part of the ContextWindowProgress component - expect(screen.getByTestId("context-tokens-count")).toBeInTheDocument() - expect(screen.getByTestId("context-tokens-count")).toHaveTextContent("1000") + expect(windowSize).toBeInTheDocument() + expect(windowSize).toHaveTextContent("4000") + + // The progress bar is now wrapped in tooltips, but we can verify the structure exists + // by checking for the progress bar container + const progressBarContainer = screen.getByTestId("context-tokens-count").parentElement + expect(progressBarContainer).toBeInTheDocument() + + // Verify the flex container has the expected structure + expect(progressBarContainer?.querySelector(".flex-1.relative")).toBeInTheDocument() }) }) diff --git a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx index 53b7c0a4ce..d6fd3013e6 100644 --- a/webview-ui/src/components/account/__tests__/AccountView.spec.tsx +++ b/webview-ui/src/components/account/__tests__/AccountView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { describe, it, expect, vi } from "vitest" import { AccountView } from "../AccountView" diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 910b671725..dc07764077 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -19,7 +19,7 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" -import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui" +import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui" import Thumbnails from "../common/Thumbnails" import ModeSelector from "./ModeSelector" @@ -1094,8 +1094,7 @@ const ChatTextArea = forwardRef(
+ })}> {label}
@@ -1106,21 +1105,25 @@ const ChatTextArea = forwardRef( })}>
- + + + ) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 77e1d88df7..17e7fce700 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -30,6 +30,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" import RooHero from "@src/components/welcome/RooHero" import RooTips from "@src/components/welcome/RooTips" +import { StandardTooltip } from "@src/components/ui" import TelemetryBanner from "../common/TelemetryBanner" import { useTaskSearch } from "../history/useTaskSearch" @@ -730,7 +731,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { @@ -1095,8 +1096,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction { return () => { - if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === 'function') { - (scrollToBottomSmooth as any).cancel() + if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") { + ;(scrollToBottomSmooth as any).cancel() } } }, [scrollToBottomSmooth]) @@ -1477,15 +1478,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction {showScrollToBottom ? (
-
{ - scrollToBottomSmooth() - disableAutoScrollRef.current = false - }} - title={t("chat:scrollToBottom")}> - -
+ +
{ + scrollToBottomSmooth() + disableAutoScrollRef.current = false + }}> + +
+
) : (
{primaryButtonText && !isStreaming && ( - handlePrimaryButtonClick(inputValue, selectedImages)}> - {primaryButtonText} - + }> + handlePrimaryButtonClick(inputValue, selectedImages)}> + {primaryButtonText} + + )} {(secondaryButtonText || isStreaming) && ( - handleSecondaryButtonClick(inputValue, selectedImages)}> - {isStreaming ? t("chat:cancel.title") : secondaryButtonText} - + }> + handleSecondaryButtonClick(inputValue, selectedImages)}> + {isStreaming ? t("chat:cancel.title") : secondaryButtonText} + + )}
)} diff --git a/webview-ui/src/components/chat/CodebaseSearchResult.tsx b/webview-ui/src/components/chat/CodebaseSearchResult.tsx index d5a8e6407f..4a2ced6178 100644 --- a/webview-ui/src/components/chat/CodebaseSearchResult.tsx +++ b/webview-ui/src/components/chat/CodebaseSearchResult.tsx @@ -1,5 +1,6 @@ import React from "react" import { vscode } from "@src/utils/vscode" +import { StandardTooltip } from "@/components/ui" interface CodebaseSearchResultProps { filePath: string @@ -23,19 +24,20 @@ const CodebaseSearchResult: React.FC = ({ filePath, s } return ( -
-
- - {filePath.split("/").at(-1)}:{startLine}-{endLine} - - - {filePath.split("/").slice(0, -1).join("/")} - + +
+
+ + {filePath.split("/").at(-1)}:{startLine}-{endLine} + + + {filePath.split("/").slice(0, -1).join("/")} + +
-
+ ) } diff --git a/webview-ui/src/components/chat/ContextWindowProgress.tsx b/webview-ui/src/components/chat/ContextWindowProgress.tsx index a5490d9d4f..1ae80bb3db 100644 --- a/webview-ui/src/components/chat/ContextWindowProgress.tsx +++ b/webview-ui/src/components/chat/ContextWindowProgress.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next" import { formatLargeNumber } from "@/utils/format" import { calculateTokenDistribution } from "@/utils/model-utils" +import { StandardTooltip } from "@/components/ui" interface ContextWindowProgressProps { contextWindow: number @@ -26,64 +27,70 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens const safeContextWindow = Math.max(0, contextWindow) const safeContextTokens = Math.max(0, contextTokens) + // Combine all tooltip content into a single tooltip + const tooltipContent = ( +
+
+ {t("chat:tokenProgress.tokensUsed", { + used: formatLargeNumber(safeContextTokens), + total: formatLargeNumber(safeContextWindow), + })} +
+ {reservedForOutput > 0 && ( +
+ {t("chat:tokenProgress.reservedForResponse", { + amount: formatLargeNumber(reservedForOutput), + })} +
+ )} + {availableSize > 0 && ( +
+ {t("chat:tokenProgress.availableSpace", { + amount: formatLargeNumber(availableSize), + })} +
+ )} +
+ ) + return ( <>
{formatLargeNumber(safeContextTokens)}
-
- {/* Invisible overlay for hover area */} -
- - {/* Main progress bar container */} -
- {/* Current tokens container */} -
- {/* Invisible overlay for current tokens section */} + +
+ {/* Main progress bar container */} +
+ {/* Current tokens container */}
- {/* Current tokens used - darkest */} -
-
+ className="relative h-full" + style={{ width: `${currentPercent}%` }} + data-testid="context-tokens-used"> + {/* Current tokens used - darkest */} +
+
- {/* Container for reserved tokens */} -
- {/* Invisible overlay for reserved section */} + {/* Container for reserved tokens */}
- {/* Reserved for output section - medium gray */} -
-
+ className="relative h-full" + style={{ width: `${reservedPercent}%` }} + data-testid="context-reserved-tokens"> + {/* Reserved for output section - medium gray */} +
+
- {/* Empty section (if any) */} - {availablePercent > 0 && ( -
- {/* Invisible overlay for available space */} + {/* Empty section (if any) */} + {availablePercent > 0 && (
-
- )} + className="relative h-full" + style={{ width: `${availablePercent}%` }} + data-testid="context-available-space-section"> + {/* Available space - transparent */} +
+ )} +
-
+
{formatLargeNumber(safeContextWindow)}
diff --git a/webview-ui/src/components/chat/FollowUpSuggest.tsx b/webview-ui/src/components/chat/FollowUpSuggest.tsx index 44a30ca803..9f4c017e74 100644 --- a/webview-ui/src/components/chat/FollowUpSuggest.tsx +++ b/webview-ui/src/components/chat/FollowUpSuggest.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react" import { Edit } from "lucide-react" -import { Button } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@src/i18n/TranslationContext" @@ -36,18 +36,19 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }: aria-label={suggestion}>
{suggestion}
-
{ - e.stopPropagation() - // Simulate shift-click by directly calling the handler with shiftKey=true. - onSuggestionClick?.(suggestion, { ...e, shiftKey: true }) - }} - title={t("chat:followUpSuggest.copyToInput")}> - -
+ +
{ + e.stopPropagation() + // Simulate shift-click by directly calling the handler with shiftKey=true. + onSuggestionClick?.(suggestion, { ...e, shiftKey: true }) + }}> + +
+
))}
diff --git a/webview-ui/src/components/chat/IconButton.tsx b/webview-ui/src/components/chat/IconButton.tsx index 34311b22c6..75d8bc4b0b 100644 --- a/webview-ui/src/components/chat/IconButton.tsx +++ b/webview-ui/src/components/chat/IconButton.tsx @@ -1,4 +1,5 @@ import { cn } from "@/lib/utils" +import { StandardTooltip } from "@/components/ui" interface IconButtonProps extends React.ButtonHTMLAttributes { iconClass: string @@ -35,10 +36,9 @@ export const IconButton: React.FC = ({ const iconClasses = cn("codicon", iconClass, isLoading && "codicon-modifier-spin") - return ( + const button = ( ) + + return {button} } diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index a209ce8723..ba838284d7 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -2,6 +2,7 @@ import { memo, useState } from "react" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import { useCopyToClipboard } from "@src/utils/clipboard" +import { StandardTooltip } from "@src/components/ui" import MarkdownBlock from "../common/MarkdownBlock" @@ -34,30 +35,31 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia borderRadius: "4px", }}> - { - const success = await copyWithFeedback(markdown) - if (success) { - const button = document.activeElement as HTMLElement - if (button) { - button.style.background = "var(--vscode-button-background)" - setTimeout(() => { - button.style.background = "" - }, 200) + + { + const success = await copyWithFeedback(markdown) + if (success) { + const button = document.activeElement as HTMLElement + if (button) { + button.style.background = "var(--vscode-button-background)" + setTimeout(() => { + button.style.background = "" + }, 200) + } } - } - }} - title="Copy as markdown"> - - + }}> + + +
)}
diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index f066dabfa6..f7fa727d2a 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -2,7 +2,7 @@ import React from "react" import { ChevronUp, Check } from "lucide-react" import { cn } from "@/lib/utils" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" import { IconButton } from "./IconButton" import { vscode } from "@/utils/vscode" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -55,6 +55,27 @@ export const ModeSelector = ({ // Find the selected mode const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value]) + const trigger = ( + + + {selectedMode?.name || ""} + + ) + return ( - - - {selectedMode?.name || ""} - + {title ? {trigger} : trigger} { {shareButtonState.showPopover ? ( - + + + {shareSuccess ? ( @@ -205,15 +207,16 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => { ) : ( - + + + )} {/* Connect to Cloud Modal */} diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index f7b163f720..bffccf7d08 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -10,7 +10,7 @@ import { getModelMaxOutputTokens } from "@roo/api" import { formatLargeNumber } from "@src/utils/format" import { cn } from "@src/lib/utils" -import { Button } from "@src/components/ui" +import { Button, StandardTooltip } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" @@ -60,13 +60,14 @@ const TaskHeader = ({ const { width: windowWidth } = useWindowSize() const condenseButton = ( - + + + ) return ( @@ -97,14 +98,11 @@ const TaskHeader = ({ )}
- + + +
{/* Collapsed state: Track context and cost if we have any */} {!isTaskExpanded && contextWindow > 0 && ( diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index 7048022440..42c2d6ff50 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { Package } from "@roo/package" diff --git a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx index 7aef88d0de..6b2a290c63 100644 --- a/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index e31803b99e..63112d12c0 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1,4 +1,4 @@ -import { render, fireEvent, screen } from "@testing-library/react" +import { render, fireEvent, screen } from "@/utils/test-utils" import { defaultModeSlug } from "@roo/modes" @@ -761,7 +761,7 @@ describe("ChatTextArea", () => { describe("selectApiConfig", () => { // Helper function to get the API config dropdown const getApiConfigDropdown = () => { - return screen.getByTitle("chat:selectApiConfig") + return screen.getByTestId("dropdown-trigger") } it("should be enabled independently of sendingDisabled", () => { render() diff --git a/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx index 3e819904fe..15405396f7 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx @@ -1,6 +1,6 @@ // npx vitest run src/components/chat/__tests__/ChatView.auto-approve.spec.tsx -import { render, waitFor } from "@testing-library/react" +import { render, waitFor } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext" diff --git a/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx index f6ecbaf0e4..5a21b6c14c 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx @@ -1,7 +1,7 @@ // npx vitest run src/components/chat/__tests__/ChatView.spec.tsx import React from "react" -import { render, waitFor, act } from "@testing-library/react" +import { render, waitFor, act } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext" diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx index e75b16606a..4ef3764841 100644 --- a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen, fireEvent, waitFor, act } from "@testing-library/react" +import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx index 4d07a6de46..d6fc81368d 100644 --- a/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import { describe, test, expect, vi } from "vitest" import ModeSelector from "../ModeSelector" import { Mode } from "@roo/modes" diff --git a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx index f6102dc616..cbe5620264 100644 --- a/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx @@ -1,5 +1,5 @@ import { describe, test, expect, vi, beforeEach } from "vitest" -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { ShareButton } from "../ShareButton" import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx index c21c5b9331..2d9ea0cac1 100644 --- a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { vi, describe, it, expect, beforeEach } from "vitest" import { TaskActions } from "../TaskActions" import type { HistoryItem } from "@roo-code/types" @@ -89,15 +89,19 @@ describe("TaskActions", () => { it("renders share button when item has id", () => { render() - const shareButton = screen.getByTitle("Share task") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) expect(shareButton).toBeInTheDocument() }) it("does not render share button when item has no id", () => { render() - const shareButton = screen.queryByTitle("Share task") - expect(shareButton).not.toBeInTheDocument() + // Find button by its icon class + const buttons = screen.queryAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).not.toBeDefined() }) it("renders share button even when not authenticated", () => { @@ -108,7 +112,9 @@ describe("TaskActions", () => { render() - const shareButton = screen.getByTitle("Share task") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) expect(shareButton).toBeInTheDocument() }) }) @@ -117,8 +123,11 @@ describe("TaskActions", () => { it("shows organization and public share options when authenticated and sharing enabled", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.getByText("Share with Organization")).toBeInTheDocument() expect(screen.getByText("Share Publicly")).toBeInTheDocument() @@ -127,8 +136,11 @@ describe("TaskActions", () => { it("sends shareCurrentTask message when organization option is selected", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) const orgOption = screen.getByText("Share with Organization") fireEvent.click(orgOption) @@ -142,8 +154,11 @@ describe("TaskActions", () => { it("sends shareCurrentTask message when public option is selected", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) const publicOption = screen.getByText("Share Publicly") fireEvent.click(publicOption) @@ -165,8 +180,11 @@ describe("TaskActions", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() expect(screen.getByText("Share Publicly")).toBeInTheDocument() @@ -184,8 +202,11 @@ describe("TaskActions", () => { it("shows connect to cloud option when not authenticated", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument() expect(screen.getByText("Sign in to Roo Code Cloud to share tasks")).toBeInTheDocument() @@ -195,8 +216,11 @@ describe("TaskActions", () => { it("does not show organization and public options when not authenticated", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() expect(screen.queryByText("Share Publicly")).not.toBeInTheDocument() @@ -205,8 +229,11 @@ describe("TaskActions", () => { it("sends rooCloudSignIn message when connect to cloud is selected", () => { render() - const shareButton = screen.getByTitle("Share task") - fireEvent.click(shareButton) + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + expect(shareButton).toBeDefined() + fireEvent.click(shareButton!) const connectOption = screen.getByText("Connect") fireEvent.click(connectOption) @@ -226,12 +253,14 @@ describe("TaskActions", () => { render() - const shareButton = screen.getByTitle("Sharing disabled by organization") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) expect(shareButton).toBeInTheDocument() expect(shareButton).toBeDisabled() // Should not have a popover when sharing is disabled - fireEvent.click(shareButton) + fireEvent.click(shareButton!) expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument() expect(screen.queryByText("Connect to Cloud")).not.toBeInTheDocument() }) @@ -269,14 +298,14 @@ describe("TaskActions", () => { it("renders export button", () => { render() - const exportButton = screen.getByTitle("Export task history") + const exportButton = screen.getByLabelText("Export task history") expect(exportButton).toBeInTheDocument() }) it("sends exportCurrentTask message when export button is clicked", () => { render() - const exportButton = screen.getByTitle("Export task history") + const exportButton = screen.getByLabelText("Export task history") fireEvent.click(exportButton) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -287,7 +316,7 @@ describe("TaskActions", () => { it("renders delete button and file size when item has size", () => { render() - const deleteButton = screen.getByTitle("Delete Task (Shift + Click to skip confirmation)") + const deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)") expect(deleteButton).toBeInTheDocument() expect(screen.getByText("1024 B")).toBeInTheDocument() }) @@ -296,7 +325,7 @@ describe("TaskActions", () => { const itemWithoutSize = { ...mockItem, size: 0 } render() - const deleteButton = screen.queryByTitle("Delete Task (Shift + Click to skip confirmation)") + const deleteButton = screen.queryByLabelText("Delete Task (Shift + Click to skip confirmation)") expect(deleteButton).not.toBeInTheDocument() }) }) @@ -305,8 +334,10 @@ describe("TaskActions", () => { it("disables buttons when buttonsDisabled is true", () => { render() - const shareButton = screen.getByTitle("Share task") - const exportButton = screen.getByTitle("Export task history") + // Find button by its icon class + const buttons = screen.getAllByRole("button") + const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + const exportButton = screen.getByLabelText("Export task history") expect(shareButton).toBeDisabled() expect(exportButton).toBeDisabled() diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 784a263531..07eec80ff6 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -1,7 +1,7 @@ // npx vitest src/components/chat/__tests__/TaskHeader.spec.tsx import React from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import type { ProviderSettings } from "@roo-code/types" @@ -94,22 +94,33 @@ describe("TaskHeader", () => { it("should render the condense context button", () => { renderTaskHeader() - expect(screen.getByTitle("chat:task.condenseContext")).toBeInTheDocument() + // Find the button that contains the FoldVertical icon + const buttons = screen.getAllByRole("button") + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) + expect(condenseButton).toBeDefined() + expect(condenseButton?.querySelector("svg")).toBeInTheDocument() }) it("should call handleCondenseContext when condense context button is clicked", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ handleCondenseContext }) - const condenseButton = screen.getByTitle("chat:task.condenseContext") - fireEvent.click(condenseButton) + // Find the button that contains the FoldVertical icon + const buttons = screen.getAllByRole("button") + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) + expect(condenseButton).toBeDefined() + fireEvent.click(condenseButton!) expect(handleCondenseContext).toHaveBeenCalledWith("test-task-id") }) it("should disable the condense context button when buttonsDisabled is true", () => { const handleCondenseContext = vi.fn() renderTaskHeader({ buttonsDisabled: true, handleCondenseContext }) - const condenseButton = screen.getByTitle("chat:task.condenseContext") - fireEvent.click(condenseButton) + // Find the button that contains the FoldVertical icon + const buttons = screen.getAllByRole("button") + const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical")) + expect(condenseButton).toBeDefined() + expect(condenseButton).toBeDisabled() + fireEvent.click(condenseButton!) expect(handleCondenseContext).not.toHaveBeenCalled() }) }) diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 348e230619..6b632a4548 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -2,7 +2,7 @@ import { useState, useCallback } from "react" import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons" import { useTranslation } from "react-i18next" -import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" import { useRooPortal } from "@/components/ui/hooks" import { vscode } from "@src/utils/vscode" @@ -48,13 +48,11 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec return (
{isDiffAvailable && ( - + + + )} {isRestoreAvailable && ( - + + +
diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index 06f929b830..2cf8fcc82e 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -6,6 +6,7 @@ import { bundledLanguages } from "shiki" import type { ShikiTransformer } from "shiki" import { ChevronDown, ChevronUp, WrapText, AlignJustify, Copy, Check } from "lucide-react" import { useAppTranslation } from "@src/i18n/TranslationContext" +import { StandardTooltip } from "@/components/ui" export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))" export const WRAPPER_ALPHA = "cc" // 80% opacity @@ -725,48 +726,55 @@ const CodeBlock = memo( } {showCollapseButton && ( - { - // Get the current code block element - const codeBlock = codeBlockRef.current // Capture ref early - // Toggle window shade state - setWindowShade(!windowShade) - - // Clear any previous timeouts - if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current) - if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current) - - // After UI updates, ensure code block is visible and update button position - collapseTimeout1Ref.current = setTimeout( - () => { - if (codeBlock) { - // Check if codeBlock element still exists - codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" }) - - // Wait for scroll to complete before updating button position - collapseTimeout2Ref.current = setTimeout(() => { - // updateCodeBlockButtonPosition itself should also check for refs if needed - updateCodeBlockButtonPosition() - collapseTimeout2Ref.current = null - }, 50) - } - collapseTimeout1Ref.current = null - }, - WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50, - ) - }} - title={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}> - {windowShade ? : } - + + { + // Get the current code block element + const codeBlock = codeBlockRef.current // Capture ref early + // Toggle window shade state + setWindowShade(!windowShade) + + // Clear any previous timeouts + if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current) + if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current) + + // After UI updates, ensure code block is visible and update button position + collapseTimeout1Ref.current = setTimeout( + () => { + if (codeBlock) { + // Check if codeBlock element still exists + codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" }) + + // Wait for scroll to complete before updating button position + collapseTimeout2Ref.current = setTimeout(() => { + // updateCodeBlockButtonPosition itself should also check for refs if needed + updateCodeBlockButtonPosition() + collapseTimeout2Ref.current = null + }, 50) + } + collapseTimeout1Ref.current = null + }, + WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50, + ) + }}> + {windowShade ? : } + + )} - setWordWrap(!wordWrap)} - title={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}> - {wordWrap ? : } - - - {showCopyFeedback ? : } - + + setWordWrap(!wordWrap)}> + {wordWrap ? : } + + + + + {showCopyFeedback ? : } + + )} diff --git a/webview-ui/src/components/common/IconButton.tsx b/webview-ui/src/components/common/IconButton.tsx index 70a66ba9f1..ac8b7ca1e2 100644 --- a/webview-ui/src/components/common/IconButton.tsx +++ b/webview-ui/src/components/common/IconButton.tsx @@ -1,3 +1,5 @@ +import { StandardTooltip } from "@/components/ui" + interface IconButtonProps { icon: string onClick?: (e: React.MouseEvent) => void @@ -31,15 +33,21 @@ export function IconButton({ const handleClick = onClick || ((_event: React.MouseEvent) => {}) - return ( + const button = ( ) + + if (title) { + return {button} + } + + return button } diff --git a/webview-ui/src/components/common/MermaidActionButtons.tsx b/webview-ui/src/components/common/MermaidActionButtons.tsx index 46ded57644..79558b9b03 100644 --- a/webview-ui/src/components/common/MermaidActionButtons.tsx +++ b/webview-ui/src/components/common/MermaidActionButtons.tsx @@ -2,6 +2,7 @@ import React from "react" import { useAppTranslation } from "@src/i18n/TranslationContext" import { IconButton } from "./IconButton" import { ZoomControls } from "./ZoomControls" +import { StandardTooltip } from "@/components/ui" interface MermaidActionButtonsProps { onZoom?: (e: React.MouseEvent) => void @@ -40,41 +41,51 @@ export const MermaidActionButtons: React.FC = ({ zoomInTitle={t("common:mermaid.buttons.zoomIn")} zoomOutTitle={t("common:mermaid.buttons.zoomOut")} /> + + { + e.stopPropagation() + onViewCode() + }} + /> + + + + + + ) + } + + return ( + <> + {onZoom && ( + + + + )} + { e.stopPropagation() onViewCode() }} - title={t("common:mermaid.buttons.viewCode")} - /> - - - ) - } - - return ( - <> - {onZoom && } - { - e.stopPropagation() - onViewCode() - }} - title={t("common:mermaid.buttons.viewCode")} - /> - - {onSave && } - {onClose && } + + + + + {onSave && ( + + + + )} + {onClose && ( + + + + )} ) } diff --git a/webview-ui/src/components/common/MermaidButton.tsx b/webview-ui/src/components/common/MermaidButton.tsx index 57d4c26b0a..ede4d0f35e 100644 --- a/webview-ui/src/components/common/MermaidButton.tsx +++ b/webview-ui/src/components/common/MermaidButton.tsx @@ -7,6 +7,7 @@ import { Modal } from "./Modal" import { TabButton } from "./TabButton" import { IconButton } from "./IconButton" import { ZoomControls } from "./ZoomControls" +import { StandardTooltip } from "@/components/ui" const MIN_ZOOM = 0.5 const MAX_ZOOM = 20 @@ -160,11 +161,9 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
- setShowModal(false)} - title={t("common:mermaid.buttons.close")} - /> + + setShowModal(false)} /> +
+ + + + + + + + ) : ( + { + e.stopPropagation() + copyWithFeedback(code, e) + }} /> - - - ) : ( - { - e.stopPropagation() - copyWithFeedback(code, e) - }} - title={t("common:mermaid.buttons.copy")} - /> + )}
diff --git a/webview-ui/src/components/common/ZoomControls.tsx b/webview-ui/src/components/common/ZoomControls.tsx index 47093ec59f..427a17c7fd 100644 --- a/webview-ui/src/components/common/ZoomControls.tsx +++ b/webview-ui/src/components/common/ZoomControls.tsx @@ -1,5 +1,6 @@ import { IconButton } from "./IconButton" import { useRef, useEffect } from "react" +import { StandardTooltip } from "@/components/ui" interface ZoomControlsProps { zoomLevel: number @@ -67,25 +68,27 @@ export function ZoomControls({ return (
- adjustZoom?.(zoomOutStep)) : undefined} - onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined} - onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - /> + + adjustZoom?.(zoomOutStep)) : undefined} + onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined} + onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + /> +
{Math.round(zoomLevel * 100)}%
- adjustZoom?.(zoomInStep)) : undefined} - onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined} - onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} - /> + + adjustZoom?.(zoomInStep)) : undefined} + onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined} + onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined} + /> +
) } diff --git a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx index a1d3849744..f5278d5cdf 100644 --- a/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx @@ -1,6 +1,6 @@ // npx vitest run src/components/common/__tests__/CodeBlock.spec.tsx -import { render, screen, fireEvent, act } from "@testing-library/react" +import { render, screen, fireEvent, act } from "@/utils/test-utils" import CodeBlock from "../CodeBlock" @@ -170,9 +170,15 @@ describe("CodeBlock", () => { codeBlock.setAttribute("data-partially-visible", "true") } - const copyButton = screen.getByTitle("Copy code") - await act(async () => { - fireEvent.click(copyButton) - }) + // Find the copy button by looking for the button containing the Copy icon + const buttons = screen.getAllByRole("button") + const copyButton = buttons.find((btn) => btn.querySelector("svg.lucide-copy")) + + expect(copyButton).toBeTruthy() + if (copyButton) { + await act(async () => { + fireEvent.click(copyButton) + }) + } }) }) diff --git a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx index 5190f7fe82..ec97e4e667 100644 --- a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import MarkdownBlock from "../MarkdownBlock" import { vi } from "vitest" diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx index 743b150aae..4243ff8d5a 100644 --- a/webview-ui/src/components/history/CopyButton.tsx +++ b/webview-ui/src/components/history/CopyButton.tsx @@ -1,7 +1,7 @@ import { useCallback } from "react" import { useClipboard } from "@/components/ui/hooks" -import { Button } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" @@ -25,14 +25,15 @@ export const CopyButton = ({ itemTask }: CopyButtonProps) => { ) return ( - + + + ) } diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx index b91f13bd50..3e99027546 100644 --- a/webview-ui/src/components/history/DeleteButton.tsx +++ b/webview-ui/src/components/history/DeleteButton.tsx @@ -1,6 +1,6 @@ import { useCallback } from "react" -import { Button } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { vscode } from "@/utils/vscode" @@ -25,14 +25,15 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => { ) return ( - + + + ) } diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx index eeba0ccaf4..fabc8d3d15 100644 --- a/webview-ui/src/components/history/ExportButton.tsx +++ b/webview-ui/src/components/history/ExportButton.tsx @@ -1,5 +1,5 @@ import { vscode } from "@/utils/vscode" -import { Button } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { useCallback } from "react" @@ -15,14 +15,15 @@ export const ExportButton = ({ itemId }: { itemId: string }) => { ) return ( - + + + ) } diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index 2d6ee5fa3d..2f156d0418 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -5,7 +5,16 @@ import { Virtuoso } from "react-virtuoso" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { Button, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui" +import { + Button, + Checkbox, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + StandardTooltip, +} from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { Tab, TabContent, TabHeader } from "../common/Tab" @@ -75,20 +84,22 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {

{t("history:history")}

- + +
diff --git a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx index 9fe49663d1..bdcff23cdd 100644 --- a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { vscode } from "@/utils/vscode" diff --git a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx index 0ba1b27a1f..ac1b39859d 100644 --- a/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/CopyButton.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { useClipboard } from "@/components/ui/hooks" diff --git a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx index 42c17f5335..19b333ab44 100644 --- a/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { DeleteButton } from "../DeleteButton" diff --git a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx index e78101f37d..f8e244e9bf 100644 --- a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { vscode } from "@/utils/vscode" diff --git a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx index 68f4407400..1dda83305c 100644 --- a/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/ExportButton.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx index 179c51b1c0..6118509702 100644 --- a/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import type { HistoryItem } from "@roo-code/types" diff --git a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx index 030c36f503..3079844aad 100644 --- a/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { useExtensionState } from "@src/context/ExtensionStateContext" diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index 9fcc11e572..9d4a939a1e 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import TaskItem from "../TaskItem" diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index f1390b7d55..661cecf122 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import TaskItemFooter from "../TaskItemFooter" diff --git a/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx index 02f554d697..090bf2521f 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import TaskItemHeader from "../TaskItemHeader" diff --git a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx index e047a81cf3..bea79814fa 100644 --- a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx +++ b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx @@ -1,4 +1,4 @@ -import { renderHook, act } from "@testing-library/react" +import { renderHook, act } from "@/utils/test-utils" import type { HistoryItem } from "@roo-code/types" diff --git a/webview-ui/src/components/marketplace/MarketplaceView.tsx b/webview-ui/src/components/marketplace/MarketplaceView.tsx index b469e886ea..b47e1aa875 100644 --- a/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -81,7 +81,7 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace const filteredTags = useMemo(() => allTags, [allTags]) return ( - +
diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx index 8e62af73c9..06078d638c 100644 --- a/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx @@ -1,6 +1,6 @@ // npx vitest run src/components/marketplace/__tests__/MarketplaceListView.spec.tsx -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import userEvent from "@testing-library/user-event" import { TooltipProvider } from "@/components/ui/tooltip" @@ -49,7 +49,7 @@ describe("MarketplaceListView", () => { const renderWithProviders = (props = {}) => render( - + , diff --git a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx index 10290b70b3..95b2dea54b 100644 --- a/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx +++ b/webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import userEvent from "@testing-library/user-event" import { MarketplaceView } from "../MarketplaceView" diff --git a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx index b59291a35b..defc138581 100644 --- a/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx +++ b/webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx @@ -7,7 +7,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext" import { isValidUrl } from "../../../utils/url" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" +import { StandardTooltip } from "@/components/ui" import { MarketplaceInstallModal } from "./MarketplaceInstallModal" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -79,37 +79,33 @@ export const MarketplaceItemCard: React.FC = ({ item,
{isInstalled ? ( /* Single Remove button when installed */ - - - - - - - - {isInstalledInProject + - + : t("marketplace:items.card.removeGlobalTooltip") + }> + + ) : ( /* Single Install button when not installed */ + + ))}
)} diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx index 8ffb15abd4..29af469f80 100644 --- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx +++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { MarketplaceItem } from "@roo-code/types" diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx index 6bc9453cd9..e586fbb9e1 100644 --- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx +++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { MarketplaceItem } from "@roo-code/types" diff --git a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx index bd88de7229..1f1ed9030b 100644 --- a/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx +++ b/webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { render, screen } from "@/utils/test-utils" import userEvent from "@testing-library/user-event" import { MarketplaceItem } from "@roo-code/types" @@ -57,7 +57,7 @@ vi.mock("@/i18n/TranslationContext", () => ({ })) const renderWithProviders = (ui: React.ReactElement) => { - return render({ui}) + return render({ui}) } describe("MarketplaceItemCard", () => { diff --git a/webview-ui/src/components/mcp/McpToolRow.tsx b/webview-ui/src/components/mcp/McpToolRow.tsx index 892304d1c6..58b938f9f1 100644 --- a/webview-ui/src/components/mcp/McpToolRow.tsx +++ b/webview-ui/src/components/mcp/McpToolRow.tsx @@ -4,6 +4,7 @@ import { McpTool } from "@roo/mcp" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" +import { StandardTooltip } from "@/components/ui" type McpToolRowProps = { tool: McpTool @@ -46,9 +47,9 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo {/* Tool name section */}
- - {tool.name} - + + {tool.name} +
{/* Controls section */} @@ -69,24 +70,25 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo {/* Enabled eye button - only show in settings context */} {!isInChatContext && ( - + + + )}
)} diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx index a3ccd6a990..2bfe3b1338 100644 --- a/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx +++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, fireEvent, screen } from "@testing-library/react" +import { render, fireEvent, screen } from "@/utils/test-utils" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/modes/ModesView.tsx b/webview-ui/src/components/modes/ModesView.tsx index 42069de8f8..2dd6c6dc76 100644 --- a/webview-ui/src/components/modes/ModesView.tsx +++ b/webview-ui/src/components/modes/ModesView.tsx @@ -45,6 +45,7 @@ import { CommandItem, CommandGroup, Input, + StandardTooltip, } from "@src/components/ui" // Get all available groups that should show in prompts view @@ -431,30 +432,29 @@ const ModesView = ({ onDone }: ModesViewProps) => {
e.stopPropagation()} className="flex justify-between items-center mb-3">

{t("prompts:modes.title")}

- -
- + +
+ + + {showConfigMenu && (
e.stopPropagation()} @@ -652,18 +652,19 @@ const ModesView = ({ onDone }: ModesViewProps) => { }} className="w-full" /> - + + +
@@ -674,19 +675,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:roleDefinition.title")}
{!findModeBySlug(visualMode, customModes) && ( - + + + )}
@@ -733,19 +735,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:description.title")}
{!findModeBySlug(visualMode, customModes) && ( - + + + )}
@@ -786,19 +789,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:whenToUse.title")}
{!findModeBySlug(visualMode, customModes) && ( - + + + )}
@@ -843,18 +847,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:tools.title")}
{findModeBySlug(visualMode, customModes) && ( - + + )}
{!findModeBySlug(visualMode, customModes) && ( @@ -936,19 +942,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
{t("prompts:customInstructions.title")}
{!findModeBySlug(visualMode, customModes) && ( - + + + )}
@@ -1041,22 +1048,23 @@ const ModesView = ({ onDone }: ModesViewProps) => { data-testid="preview-prompt-button"> {t("prompts:systemPrompt.preview")} - + + +
{/* Custom System Prompt Disclosure */} diff --git a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx index 4ff7f4cf87..e202114bbb 100644 --- a/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx +++ b/webview-ui/src/components/modes/__tests__/ModesView.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/modes/__tests__/ModesView.spec.tsx -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import ModesView from "../ModesView" import { ExtensionStateContext } from "@src/context/ExtensionStateContext" import { vscode } from "@src/utils/vscode" diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index d0a0a6aa44..edd0a5fe07 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -21,6 +21,7 @@ import { Popover, PopoverContent, PopoverTrigger, + StandardTooltip, } from "@/components/ui" interface ApiConfigManagerProps { @@ -248,23 +249,25 @@ const ApiConfigManager = ({ }} className="grow" /> - - + + + + + +
{error && (
@@ -334,13 +337,17 @@ const ApiConfigManager = ({ className={!valid ? "text-vscode-errorForeground" : ""}>
{!valid && ( - - - + + + + + )} {config.name}
@@ -360,37 +367,37 @@ const ApiConfigManager = ({ - + + + {currentApiConfigName && ( <> - - + + - - + }> + + )}
diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index ffad47e2ac..d2b6694f75 100644 --- a/webview-ui/src/components/settings/AutoApproveToggle.tsx +++ b/webview-ui/src/components/settings/AutoApproveToggle.tsx @@ -2,7 +2,7 @@ import type { GlobalSettings } from "@roo-code/types" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" -import { Button } from "@/components/ui" +import { Button, StandardTooltip } from "@/components/ui" type AutoApproveToggles = Pick< GlobalSettings, @@ -100,20 +100,20 @@ export const AutoApproveToggle = ({ onToggle, ...props }: AutoApproveToggleProps "[@media(min-width:800px)]:max-w-[800px]", )}> {Object.values(autoApproveSettingsConfig).map(({ key, descriptionKey, labelKey, icon, testId }) => ( - + + + ))}
) diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index 568b8eeee1..160f79dc84 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -6,7 +6,15 @@ import { supportPrompt, SupportPromptType } from "@roo/support-prompt" import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" +import { + Button, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + StandardTooltip, +} from "@src/components/ui" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { MessageSquare } from "lucide-react" @@ -97,15 +105,14 @@ const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: Prom
- + +
(({ onDone, t

{t("settings:header.title")}

- - + }> + + + + +
@@ -510,7 +512,7 @@ const SettingsView = forwardRef(({ onDone, t if (isCompactMode) { // Wrap in Tooltip and manually add onClick to the trigger return ( - + {/* Clone to avoid ref issues if triggerComponent itself had a key */} diff --git a/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx index 553f60c79e..194f0ff31e 100644 --- a/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ApiConfigManager.spec.tsx -import { render, screen, fireEvent, within } from "@testing-library/react" +import { render, screen, fireEvent, within } from "@/utils/test-utils" import ApiConfigManager from "../ApiConfigManager" @@ -41,6 +41,7 @@ vitest.mock("@/components/ui", () => ({ data-testid={dataTestId} /> ), + StandardTooltip: ({ children, content }: any) =>
{children}
, // New components for searchable dropdown Popover: ({ children, open }: any) => (
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index 31cc2ec82f..1e13323f36 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ApiOptions.spec.tsx -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types" @@ -61,6 +61,7 @@ vi.mock("@/components/ui", () => ({ {children} ), + StandardTooltip: ({ children, content }: any) =>
{children}
, // Add missing components used by ModelPicker Command: ({ children }: any) =>
{children}
, CommandEmpty: ({ children }: any) =>
{children}
, diff --git a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx index 4442cb9abb..97e5dcc96b 100644 --- a/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" diff --git a/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx index 7e591114a8..3c3dae7956 100644 --- a/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/CodeIndexSettings.spec.tsx -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import userEvent from "@testing-library/user-event" import { CodeIndexSettings } from "../CodeIndexSettings" diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 9a7d451d1d..e467ab51d6 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { ContextManagementSettings } from "@src/components/settings/ContextManagementSettings" diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx index 38237dc0e3..82c6da2a3f 100644 --- a/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ModelPicker.spec.tsx -import { screen, fireEvent, render } from "@testing-library/react" +import { screen, fireEvent, render } from "@/utils/test-utils" import { act } from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { vi } from "vitest" diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 8d79b998a1..0a76e54ffe 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { vscode } from "@/utils/vscode" @@ -115,6 +115,7 @@ vi.mock("@/components/ui", () => ({ {children} ), + StandardTooltip: ({ children, content }: any) =>
{children}
, Input: ({ value, onChange, placeholder, "data-testid": dataTestId }: any) => ( ), diff --git a/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx b/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx index ad4e352126..f9eba66c8f 100644 --- a/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/TemperatureControl.spec.tsx -import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" import { TemperatureControl } from "../TemperatureControl" diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index f5d1cf29dd..5ca51b4528 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -1,6 +1,6 @@ // npx vitest src/components/settings/__tests__/ThinkingBudget.spec.tsx -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import type { ModelInfo } from "@roo-code/types" diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx index a0ebafd88e..1839298f9b 100644 --- a/webview-ui/src/components/settings/providers/Bedrock.tsx +++ b/webview-ui/src/components/settings/providers/Bedrock.tsx @@ -5,7 +5,7 @@ import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview- import { type ProviderSettings, type ModelInfo, BEDROCK_REGIONS } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui" import { inputEventTransform, noTransform } from "../transforms" @@ -114,11 +114,12 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo onChange={handleInputChange("awsUsePromptCache", noTransform)}>
{t("settings:providers.enablePromptCaching")} - + + +
diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index b5f7abc7d4..736b0253c4 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -15,7 +15,7 @@ import { import { ExtensionMessage } from "@roo/ExtensionMessage" import { useAppTranslation } from "@src/i18n/TranslationContext" -import { Button } from "@src/components/ui" +import { Button, StandardTooltip } from "@src/components/ui" import { convertHeadersToObject } from "../utils/headers" import { inputEventTransform, noTransform } from "../transforms" @@ -208,9 +208,11 @@ export const OpenAICompatible = ({
- - - + + + + +
{!customHeaders.length ? (
@@ -231,12 +233,11 @@ export const OpenAICompatible = ({ placeholder={t("settings:providers.headerValue")} onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)} /> - handleRemoveCustomHeader(index)}> - - + + handleRemoveCustomHeader(index)}> + + +
)) )} @@ -305,7 +306,6 @@ export const OpenAICompatible = ({ return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" })(), }} - title={t("settings:providers.customModel.maxTokens.description")} onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = parseInt((e.target as HTMLInputElement).value) @@ -344,7 +344,6 @@ export const OpenAICompatible = ({ return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)" })(), }} - title={t("settings:providers.customModel.contextWindow.description")} onInput={handleInputChange("openAiCustomModelInfo", (e) => { const value = (e.target as HTMLInputElement).value const parsed = parseInt(value) @@ -382,11 +381,12 @@ export const OpenAICompatible = ({ {t("settings:providers.customModel.imageSupport.label")} - + + +
{t("settings:providers.customModel.imageSupport.description")} @@ -405,11 +405,12 @@ export const OpenAICompatible = ({ })}> {t("settings:providers.customModel.computerUse.label")} - + + +
{t("settings:providers.customModel.computerUse.description")} @@ -428,11 +429,12 @@ export const OpenAICompatible = ({ })}> {t("settings:providers.customModel.promptCache.label")} - + + +
{t("settings:providers.customModel.promptCache.description")} @@ -473,11 +475,12 @@ export const OpenAICompatible = ({ - + + +
@@ -516,11 +519,12 @@ export const OpenAICompatible = ({ - + + +
@@ -559,11 +563,13 @@ export const OpenAICompatible = ({ {t("settings:providers.customModel.pricing.cacheReads.label")} - + + +
@@ -599,11 +605,13 @@ export const OpenAICompatible = ({ - + + + diff --git a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx index ac99a12c6c..b5bb16e975 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { Bedrock } from "../Bedrock" import { ProviderSettings } from "@roo-code/types" diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx index c96de176e6..aba81ec219 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx @@ -1,5 +1,5 @@ import React from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { OpenAICompatible } from "../OpenAICompatible" import { ProviderSettings } from "@roo-code/types" @@ -64,6 +64,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ // Mock the UI components vi.mock("@src/components/ui", () => ({ Button: ({ children, onClick }: any) => , + StandardTooltip: ({ children, content }: any) =>
{children}
, })) // Mock other components diff --git a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx index d4ed834654..2262adb872 100644 --- a/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx +++ b/webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx @@ -1,7 +1,7 @@ // npx vitest run src/components/ui/__tests__/select-dropdown.spec.tsx import { ReactNode } from "react" -import { render, screen, fireEvent } from "@testing-library/react" +import { render, screen, fireEvent } from "@/utils/test-utils" import { SelectDropdown, DropdownOptionType } from "../select-dropdown" diff --git a/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx b/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx new file mode 100644 index 0000000000..e1cb619545 --- /dev/null +++ b/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx @@ -0,0 +1,182 @@ +import { render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { describe, it, expect } from "vitest" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip" +import { StandardTooltip } from "../standard-tooltip" + +describe("Tooltip", () => { + it("should render tooltip content on hover", async () => { + const user = userEvent.setup() + + render( + + + Hover me + Tooltip text + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + expect(tooltips.length).toBeGreaterThan(0) + }, + { timeout: 1000 }, + ) + }) + + it("should apply text wrapping classes", async () => { + const user = userEvent.setup() + + render( + + + Hover me + + This is a very long tooltip text that should wrap when it reaches the maximum width + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText(/This is a very long tooltip text/) + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words") + }, + { timeout: 1000 }, + ) + }) + + it("should not have overflow-hidden class", async () => { + const user = userEvent.setup() + + render( + + + Hover me + Tooltip text + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).not.toHaveClass("overflow-hidden") + }, + { timeout: 1000 }, + ) + }) +}) + +describe("StandardTooltip", () => { + it("should render with default delay", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + expect(tooltips.length).toBeGreaterThan(0) + }, + { timeout: 1000 }, + ) + }) + + it("should apply custom maxWidth", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Long tooltip text") + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveStyle({ maxWidth: "200px" }) + }, + { timeout: 1000 }, + ) + }) + + it("should apply custom maxWidth as string", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Long tooltip text") + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveStyle({ maxWidth: "15rem" }) + }, + { timeout: 1000 }, + ) + }) + + it("should handle long content with text wrapping", async () => { + const user = userEvent.setup() + const longContent = + "This is a very long tooltip content that should definitely wrap when displayed because it exceeds the maximum width constraint" + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText(longContent) + const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip") + expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words") + }, + { timeout: 1000 }, + ) + }) +}) diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index 69d9c093e0..5bd6b62ee8 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -16,3 +16,4 @@ export * from "./select-dropdown" export * from "./select" export * from "./textarea" export * from "./tooltip" +export * from "./standard-tooltip" diff --git a/webview-ui/src/components/ui/select-dropdown.tsx b/webview-ui/src/components/ui/select-dropdown.tsx index 7fcc6884b7..d9ee99ebbb 100644 --- a/webview-ui/src/components/ui/select-dropdown.tsx +++ b/webview-ui/src/components/ui/select-dropdown.tsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils" import { useRooPortal } from "./hooks/useRooPortal" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui" +import { StandardTooltip } from "@/components/ui" export enum DropdownOptionType { ITEM = "item", @@ -185,25 +186,28 @@ export const SelectDropdown = React.memo( [onChange, options], ) + const triggerContent = ( + + + {displayText} + + ) + return ( - - - {displayText} - + {title ? {triggerContent} : triggerContent} + * + * + * + * // With custom positioning + * + * + * + * + * @note This replaces native HTML title attributes for consistent timing. + * @note Requires a TooltipProvider to be present in the component tree (typically at the app root). + * @note Do not nest StandardTooltip components as this can cause UI issues. + */ +export function StandardTooltip({ + children, + content, + side = "top", + align = "center", + sideOffset = 4, + className, + asChild = true, + maxWidth, +}: StandardTooltipProps) { + // Don't render tooltip if content is empty or only whitespace + if (!content || (typeof content === "string" && !content.trim())) { + return <>{children} + } + + const style = maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : undefined + + return ( + + {children} + + {content} + + + ) +} diff --git a/webview-ui/src/components/ui/tooltip.tsx b/webview-ui/src/components/ui/tooltip.tsx index 7818d9b125..d09d8cec9d 100644 --- a/webview-ui/src/components/ui/tooltip.tsx +++ b/webview-ui/src/components/ui/tooltip.tsx @@ -17,8 +17,11 @@ const TooltipContent = React.forwardRef< { + return {children} +} + +const customRender = (ui: React.ReactElement, options?: Omit) => + render(ui, { wrapper: AllTheProviders, ...options }) + +// re-export everything +export * from "@testing-library/react" + +// override render method +export { customRender as render }