diff --git a/frontend/src/components/features/sidebar/user-actions.tsx b/frontend/src/components/features/sidebar/user-actions.tsx index aaa766c885..27b1543b07 100644 --- a/frontend/src/components/features/sidebar/user-actions.tsx +++ b/frontend/src/components/features/sidebar/user-actions.tsx @@ -58,6 +58,9 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) { className={cn( "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto", showMenu && "opacity-100 pointer-events-auto", + // Invisible hover bridge: extends hover zone to create a "safe corridor" + // for diagonal mouse movement to the menu (only active when menu is visible) + "group-hover:before:absolute group-hover:before:bottom-0 group-hover:before:right-0 group-hover:before:w-[200px] group-hover:before:h-[300px]", )} > { + // Skip on WebKit - Playwright's mouse.move() doesn't reliably trigger CSS hover states + test.skip(browserName === "webkit", "Playwright hover simulation unreliable"); + + await page.goto("/"); + + // Get the user avatar button + const userAvatar = page.getByTestId("user-avatar"); + await expect(userAvatar).toBeVisible(); + + // Get avatar bounding box first + const avatarBox = await userAvatar.boundingBox(); + if (!avatarBox) { + throw new Error("Could not get bounding box for avatar"); + } + + // Use mouse.move to hover (not .hover() which may trigger click) + const avatarCenterX = avatarBox.x + avatarBox.width / 2; + const avatarCenterY = avatarBox.y + avatarBox.height / 2; + await page.mouse.move(avatarCenterX, avatarCenterY); + + // The context menu should appear via CSS group-hover + const contextMenu = page.getByTestId("account-settings-context-menu"); + await expect(contextMenu).toBeVisible(); + + // Move UP from the LEFT side of the avatar - simulating diagonal movement + // toward the menu (which is to the right). This exits the hover zone. + const leftX = avatarBox.x + 2; + const aboveY = avatarBox.y - 50; + await page.mouse.move(leftX, aboveY); + + // The menu uses opacity-0/opacity-100 for visibility via CSS. + // Use toHaveCSS which auto-retries, avoiding flaky waitForTimeout. + // The menu should remain visible (opacity 1) to allow diagonal access to it. + const menuWrapper = contextMenu.locator(".."); + await expect(menuWrapper).toHaveCSS("opacity", "1"); +});