Repository: All-Hands-AI/OpenHands
File: frontend/tests/avatar-menu.spec.ts (TypeScript)

Code before:
 */
test("avatar context menu stays open when moving cursor diagonally to menu", async ({
  page,
  browserName,
}) => {
  // WebKit: Playwright hover/mouse simulation is flaky for CSS hover-only menus.
  test.skip(browserName === "webkit", "Playwright hover simulation unreliable");

  await page.goto("/");

  const aiConfigModal = page.getByTestId("ai-config-modal");
  if (await aiConfigModal.isVisible().catch(() => false)) {
    // In OSS mock mode, missing settings can open the AI-config modal; its backdrop
    // intercepts pointer events and prevents hover interactions.
    await page.getByTestId("save-settings-button").click();
    await expect(aiConfigModal).toBeHidden();
  }

  const userAvatar = page.getByTestId("user-avatar");
  await expect(userAvatar).toBeVisible();

  // Ensure the avatar is in the viewport
  await userAvatar.scrollIntoViewIfNeeded();

  // Wait for the avatar to be fully rendered
  await page.waitForTimeout(100);

  const contextMenu = page.getByTestId("account-settings-context-menu");

  // Use click to reliably open the menu (component toggles state on click).
  // This is more deterministic in headless CI than relying on CSS :hover.
  await userAvatar.click();

  // Wait for the context menu to become visible with a generous timeout
  await expect(contextMenu).toBeVisible({ timeout: 10000 });

  // Validate the menu has non-zero dimensions (is actually rendered/interactable)
  const menuBox = await contextMenu.boundingBox();
  expect(menuBox).not.toBeNull();
  expect(menuBox!.width).toBeGreaterThan(0);
  expect(menuBox!.height).toBeGreaterThan(0);
});


Review comment (bug):
> Note that `page.waitForTimeout()` should only be used for debugging. Tests using the timer in production are going to be flaky. Use signals such as network events, selectors becoming visible and others instead.

I'd rather undo the changes here for now and address this in a separate PR

Code after:
 *
 * The component supports both CSS hover and click-to-toggle for the menu.
 * We use click-to-toggle which is more reliable in automated tests than
 * CSS hover simulation.
 */
test("avatar context menu stays open when moving cursor diagonally to menu", async ({
  page,
}) => {
  await page.goto("/");

  // Wait for the page to be fully loaded and check for AI config modal
  // The modal may appear for new users in OSS mode without settings
  const aiConfigModal = page.getByTestId("ai-config-modal");

  // Give the modal a chance to appear (it may load asynchronously)
  try {
    await aiConfigModal.waitFor({ state: "visible", timeout: 3000 });
    // Modal appeared - dismiss it by clicking save
    await page.getByTestId("save-settings-button").click();
    await expect(aiConfigModal).toBeHidden();
  } catch {
    // Modal didn't appear within timeout - that's fine, continue with test
  }

  const userAvatar = page.getByTestId("user-avatar");
  await expect(userAvatar).toBeVisible();

  // Use force:true to bypass the hover bridge pseudo-element that can
  // intercept clicks when the mouse triggers group-hover state
  await userAvatar.click({ force: true });

  const contextMenu = page.getByTestId("account-settings-context-menu");
  await expect(contextMenu).toBeVisible();

  const menuWrapper = contextMenu.locator("..");
  await expect(menuWrapper).toHaveCSS("opacity", "1");

  // Now test diagonal mouse movement - move from avatar to menu
  // The menu should stay open due to the hover bridge in the component
  const avatarBox = await userAvatar.boundingBox();
  const menuBox = await contextMenu.boundingBox();

  if (!avatarBox || !menuBox) {
    throw new Error("Could not get bounding boxes");
  }

  // Move diagonally from avatar center toward the menu
  const startX = avatarBox.x + avatarBox.width / 2;
  const startY = avatarBox.y + avatarBox.height / 2;
  const endX = menuBox.x + menuBox.width / 2;
  const endY = menuBox.y + menuBox.height / 2;