chromium/chrome/browser/lacros/popup_lacros_browsertest.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <optional>

#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/test_future.h"
#include "base/timer/timer.h"
#include "chrome/browser/lacros/browser_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/crosapi/mojom/test_controller.mojom-test-utils.h"
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "content/public/test/browser_test.h"
#include "ui/aura/window.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/views/test/widget_test.h"

namespace {

// Waits for the window identified by |window_id| to have its ash-side window
// position in DIP screen coordinates set to |target_position|.
void WaitForWindowPositionInScreen(const std::string& window_id,
                                   const gfx::Point& target_position) {
  base::test::TestFuture<const std::optional<gfx::Point>&> future;
  ASSERT_TRUE(base::test::RunUntil([&]() {
    chromeos::LacrosService::Get()
        ->GetRemote<crosapi::mojom::TestController>()
        ->GetWindowPositionInScreen(window_id, future.GetCallback());
    return future.Take() == target_position;
  }));
}

void SendLongPress(const std::string& window_id,
                   const gfx::PointF& location_in_window) {
  base::test::TestFuture<void> future;

  // Generate a touch press in ash, because the bug requires lacros to receive
  // events over the Wayland connection from ash.
  auto& test_controller = chromeos::LacrosService::Get()
                              ->GetRemote<crosapi::mojom::TestController>();
  test_controller->SendTouchEvent(
      window_id, crosapi::mojom::TouchEventType::kPressed,
      /*pointer_id=*/0u, location_in_window, future.GetCallback());
  future.Get();

  // Wait long enough that the gesture recognizer will decide this is a long
  // press gesture. We cannot directly inject a long press from ash because
  // Wayland only transports press/move/release events, not gestures.
  base::RunLoop loop2;
  base::OneShotTimer timer2;
  timer2.Start(FROM_HERE, base::Seconds(1), loop2.QuitClosure());
  loop2.Run();

  // Release the touch in ash.
  test_controller->SendTouchEvent(
      window_id, crosapi::mojom::TouchEventType::kReleased,
      /*pointer_id=*/0u, location_in_window, future.GetCallback());
  future.Get();
}

using PopupBrowserTest = InProcessBrowserTest;

// Regression test for https://crbug.com/1157664. Verifying that opening a
// menu via long-press on a tab does not result in a popup window with empty
// bounds. In bug caused a Wayland protocol error and lacros crash.
IN_PROC_BROWSER_TEST_F(PopupBrowserTest, LongPressOnTabOpensNonEmptyMenu) {
  auto* lacros_service = chromeos::LacrosService::Get();
  ASSERT_TRUE(lacros_service->IsAvailable<crosapi::mojom::TestController>());
  // This test requires the tablet mode API.
  if (lacros_service->GetInterfaceVersion<crosapi::mojom::TestController>() <
      3) {
    LOG(WARNING) << "Unsupported ash version.";
    return;
  }

  // Ensure the browser is maximized. The bug only occurs when the tab strip is
  // near the top of the screen.
  browser()->window()->Maximize();

  // Wait for the window to be globally positioned at 0,0. It will eventually
  // have this position because it is maximized. We cannot assert the position
  // lacros-side because Wayland clients do not know the position of their
  // windows on the display.
  std::string window_id = lacros_window_utility::GetRootWindowUniqueId(
      browser()->window()->GetNativeWindow()->GetRootWindow());
  WaitForWindowPositionInScreen(window_id, gfx::Point(0, 0));

  // Precondition: The browser is the only open widget.
  std::set<raw_ptr<views::Widget, SetExperimental>> initial_widgets =
      views::test::WidgetTest::GetAllWidgets();
  ASSERT_EQ(1u, initial_widgets.size());
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::Widget* browser_widget = browser_view->GetWidget();
  ASSERT_EQ(browser_widget, *initial_widgets.begin());

  // Find the center position of the first tab. We cannot use "screen"
  // coordinates because the Wayland client does not know where its window is
  // located on the screen. Use widget-relative position instead.
  Tab* tab = browser_view->tabstrip()->tab_at(0);
  gfx::Point tab_center_in_widget = tab->GetLocalBounds().CenterPoint();
  views::View::ConvertPointToWidget(tab, &tab_center_in_widget);

  SendLongPress(window_id, gfx::PointF(tab_center_in_widget));

  // Wait for the popup menu to be created and positioned on screen.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    std::set<raw_ptr<views::Widget, SetExperimental>> widgets =
        views::test::WidgetTest::GetAllWidgets();
    widgets.erase(browser_widget);
    if (widgets.size() == 0u) {
      return false;
    }
    // The popup was created.
    views::Widget* popup = *widgets.begin();
    // The popup's top edge may be off the top of the screen. Wait for it to
    // be positioned on screen. The bug involved the repositioning code. We
    // know that 0 means the top of the screen because the window is
    // maximized.
    return popup->GetRestoredBounds().y() >= 0;
  }));

  // Find the popup.
  std::set<raw_ptr<views::Widget, SetExperimental>> widgets =
      views::test::WidgetTest::GetAllWidgets();
  widgets.erase(browser_widget);
  ASSERT_EQ(1u, widgets.size());
  views::Widget* popup = *widgets.begin();

  // The popup has valid bounds.
  gfx::Rect popup_bounds = popup->GetRestoredBounds();
  EXPECT_FALSE(popup_bounds.IsEmpty()) << popup_bounds.ToString();
}

}  // namespace