diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx index 1bc51026662..c70392e630c 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx @@ -18,6 +18,7 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin const isCurrent = currentHash === props.commitHash const [isPopoverOpen, setIsPopoverOpen] = useState(false) const [isClosing, setIsClosing] = useState(false) + const [isHovering, setIsHovering] = useState(false) const closeTimer = useRef(null) useEffect(() => { @@ -46,7 +47,16 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin } } - const menuVisible = isPopoverOpen || isClosing + const handleMouseEnter = () => { + setIsHovering(true) + } + + const handleMouseLeave = () => { + setIsHovering(false) + } + + // Menu is visible when hovering, popover is open, or briefly after popover closes + const menuVisible = isHovering || isPopoverOpen || isClosing const metadata = useMemo(() => { if (!checkpoint) { @@ -67,7 +77,10 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin } return ( -
+
{t("chat:checkpoint.regular")} @@ -80,10 +93,8 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin "linear-gradient(90deg, rgba(0, 188, 255, .65), rgba(0, 188, 255, .65) 80%, rgba(0, 188, 255, 0) 99%)", }}> - {/* Keep menu visible while popover is open or briefly after close to prevent jump */} -
+ {/* Keep menu visible while hovering, popover is open, or briefly after close to prevent jump */} +
{ } }) -import { render, waitFor, screen } from "@/utils/test-utils" +import { render, waitFor, screen, fireEvent } from "@/utils/test-utils" import React from "react" import userEvent from "@testing-library/user-event" import { CheckpointSaved } from "../CheckpointSaved" @@ -54,7 +54,7 @@ describe("CheckpointSaved popover visibility", () => { const getMenu = () => getByTestId("checkpoint-menu-container") as HTMLElement - // Initially hidden (relies on group-hover) + // Initially hidden (not hovering) expect(getMenu()).toBeTruthy() expect(getMenu().className).toContain("hidden") @@ -80,7 +80,12 @@ describe("CheckpointSaved popover visibility", () => { }) it("resets confirm state when popover closes", async () => { - const { getByTestId } = render() + const { getByTestId, container } = render() + const getParentDiv = () => + container.querySelector("[class*='flex items-center justify-between']") as HTMLElement + + // Hover to make menu visible + fireEvent.mouseEnter(getParentDiv()) // Open the popover await waitForOpenHandler() @@ -106,10 +111,12 @@ describe("CheckpointSaved popover visibility", () => { }) it("closes popover after preview and after confirm restore", async () => { - const { getByTestId } = render() + const { getByTestId, container } = render() const popoverRoot = () => getByTestId("restore-popover") const menuContainer = () => getByTestId("checkpoint-menu-container") + const getParentDiv = () => + container.querySelector("[class*='flex items-center justify-between']") as HTMLElement // Open await waitForOpenHandler() @@ -125,11 +132,16 @@ describe("CheckpointSaved popover visibility", () => { expect(popoverRoot().getAttribute("data-open")).toBe("false") expect(menuContainer().className).toContain("block") }) + + // Simulate mouse leaving the component to trigger hide + fireEvent.mouseLeave(getParentDiv()) + await waitFor(() => { expect(menuContainer().className).toContain("hidden") }) - // Reopen + // Hover to make menu visible again, then reopen + fireEvent.mouseEnter(getParentDiv()) lastOnOpenChange?.(true) await waitFor(() => { expect(popoverRoot().getAttribute("data-open")).toBe("true") @@ -141,8 +153,36 @@ describe("CheckpointSaved popover visibility", () => { await waitFor(() => { expect(popoverRoot().getAttribute("data-open")).toBe("false") }) + + // Simulate mouse leaving the component to trigger hide + fireEvent.mouseLeave(getParentDiv()) + await waitFor(() => { expect(menuContainer().className).toContain("hidden") }) }) + + it("shows menu on hover and hides when mouse leaves", async () => { + const { getByTestId, container } = render() + + const getMenu = () => getByTestId("checkpoint-menu-container") as HTMLElement + const getParentDiv = () => + container.querySelector("[class*='flex items-center justify-between']") as HTMLElement + + // Initially hidden (not hovering) + expect(getMenu().className).toContain("hidden") + + // Hover over the component + fireEvent.mouseEnter(getParentDiv()) + await waitFor(() => { + expect(getMenu().className).toContain("block") + expect(getMenu().className).not.toContain("hidden") + }) + + // Mouse leaves the component + fireEvent.mouseLeave(getParentDiv()) + await waitFor(() => { + expect(getMenu().className).toContain("hidden") + }) + }) })