chromium/chrome/browser/ui/views/frame/browser_frame_view_browsertest_win.cc

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

#include <tuple>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/app_menu_button.h"
#include "chrome/browser/ui/views/frame/browser_caption_button_container_win.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_win.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/windows_caption_button.h"
#include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_test_helper.h"
#include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h"
#include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_toolbar_button_container.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/win/titlebar_config.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/view_utils.h"

class BrowserFrameViewWinTest : public InProcessBrowserTest {
 public:
  BrowserFrameViewWinTest() = default;
  BrowserFrameViewWinTest(const BrowserFrameViewWinTest&) = delete;
  BrowserFrameViewWinTest& operator=(const BrowserFrameViewWinTest&) = delete;
  ~BrowserFrameViewWinTest() override = default;

 protected:
  BrowserFrameViewWin* GetBrowserFrameViewWin() {
    auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
    views::NonClientFrameView* frame_view =
        browser_view->GetWidget()->non_client_view()->frame_view();

    if (!views::IsViewClass<BrowserFrameViewWin>(frame_view)) {
      return nullptr;
    }
    return static_cast<BrowserFrameViewWin*>(frame_view);
  }

  const WindowsCaptionButton* GetMaximizeButton() {
    auto* frame_view = GetBrowserFrameViewWin();
    if (!frame_view) {
      return nullptr;
    }
    auto* caption_button_container =
        frame_view->caption_button_container_for_testing();
    return static_cast<const WindowsCaptionButton*>(
        caption_button_container->GetViewByID(VIEW_ID_MAXIMIZE_BUTTON));
  }
  bool BrowserUsingCustomDrawTitlebar() const {
    return ShouldBrowserCustomDrawTitlebar(
        BrowserView::GetBrowserViewForBrowser(browser()));
  }
};

// Test that in touch mode, the maximize button is enabled for a non-maximized
// window.
IN_PROC_BROWSER_TEST_F(BrowserFrameViewWinTest,
                       NonMaximizedTouchMaximizeButtonState) {
  ui::TouchUiController::TouchUiScoperForTesting touch_ui_scoper_{true};
  auto* maximize_button = GetMaximizeButton();
  if (!maximize_button || !BrowserUsingCustomDrawTitlebar()) {
    GTEST_SKIP() << "No maximize button or not using a custom titlebar";
  }

  EXPECT_TRUE(maximize_button->GetVisible());
  EXPECT_TRUE(maximize_button->GetEnabled());
}

// Test that in touch mode, the maximize button is disabled and not visible for
// a maximized window.
IN_PROC_BROWSER_TEST_F(BrowserFrameViewWinTest,
                       MaximizedTouchMaximizeButtonState) {
  ui::TouchUiController::TouchUiScoperForTesting touch_ui_scoper_{true};
  auto* frame_view = GetBrowserFrameViewWin();
  if (!frame_view || !BrowserUsingCustomDrawTitlebar()) {
    GTEST_SKIP() << "Chrome is not using a custom titlebar";
  }

  frame_view->frame()->Maximize();

  auto* maximize_button = GetMaximizeButton();

  // Button isn't visible, and should be disabled.
  EXPECT_FALSE(maximize_button->GetEnabled());
  EXPECT_FALSE(maximize_button->GetVisible());
}

// Test that in non touch mode, the maximize button is enabled for a
// non-maximized window.
IN_PROC_BROWSER_TEST_F(BrowserFrameViewWinTest,
                       NonTouchNonMaximizedMaximizeButtonState) {
  ui::TouchUiController::TouchUiScoperForTesting touch_ui_scoper_{false};
  auto* maximize_button = GetMaximizeButton();
  if (!maximize_button || !BrowserUsingCustomDrawTitlebar()) {
    GTEST_SKIP() << "No maximize button or not using a custom titlebar";
  }

  EXPECT_TRUE(maximize_button->GetVisible());
  EXPECT_TRUE(maximize_button->GetEnabled());
}

// Test that in non touch mode, the maximize button is enabled and not visible
// for a maximized window.
IN_PROC_BROWSER_TEST_F(BrowserFrameViewWinTest,
                       NonTouchMaximizedMaximizeButtonState) {
  ui::TouchUiController::TouchUiScoperForTesting touch_ui_scoper_{false};
  auto* frame_view = GetBrowserFrameViewWin();
  if (!frame_view || !BrowserUsingCustomDrawTitlebar()) {
    GTEST_SKIP() << "Chrome is not using a custom titlebar";
  }

  frame_view->frame()->Maximize();

  auto* maximize_button = GetMaximizeButton();
  EXPECT_FALSE(maximize_button->GetVisible());
  EXPECT_TRUE(maximize_button->GetEnabled());
}

class WebAppBrowserFrameViewWinTest : public InProcessBrowserTest {
 public:
  WebAppBrowserFrameViewWinTest() = default;
  WebAppBrowserFrameViewWinTest(const WebAppBrowserFrameViewWinTest&) = delete;
  WebAppBrowserFrameViewWinTest& operator=(
      const WebAppBrowserFrameViewWinTest&) = delete;
  ~WebAppBrowserFrameViewWinTest() override = default;

  GURL GetStartURL() {
    return embedded_test_server()->GetURL("/web_apps/no_manifest.html");
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    CHECK(embedded_test_server()->Start());
    WebAppToolbarButtonContainer::DisableAnimationForTesting(true);
  }

  void InstallAndLaunchWebApp() {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(GetStartURL());
    if (theme_color_) {
      web_app_info->theme_color = *theme_color_;
    }

    webapps::AppId app_id = web_app::test::InstallWebApp(
        browser()->profile(), std::move(web_app_info));
    content::TestNavigationObserver navigation_observer(GetStartURL());
    navigation_observer.StartWatchingNewWebContents();
    app_browser_ = web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
    navigation_observer.WaitForNavigationFinished();

    browser_view_ = BrowserView::GetBrowserViewForBrowser(app_browser_);
    views::NonClientFrameView* frame_view =
        browser_view_->GetWidget()->non_client_view()->frame_view();

    frame_view_ = static_cast<BrowserFrameViewWin*>(frame_view);

    web_app_frame_toolbar_ = browser_view_->web_app_frame_toolbar_for_testing();
    DCHECK(web_app_frame_toolbar_);
    DCHECK(web_app_frame_toolbar_->GetVisible());
  }

  std::optional<SkColor> theme_color_ = SK_ColorBLUE;
  raw_ptr<Browser, AcrossTasksDanglingUntriaged> app_browser_ = nullptr;
  raw_ptr<BrowserView, AcrossTasksDanglingUntriaged> browser_view_ = nullptr;
  raw_ptr<BrowserFrameViewWin, AcrossTasksDanglingUntriaged> frame_view_ =
      nullptr;
  raw_ptr<WebAppFrameToolbarView, AcrossTasksDanglingUntriaged>
      web_app_frame_toolbar_ = nullptr;

 private:
  web_app::OsIntegrationTestOverrideBlockingRegistration faked_os_integration_;
};

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinTest, ThemeColor) {
  InstallAndLaunchWebApp();

  EXPECT_EQ(frame_view_->GetTitlebarColor(), *theme_color_);
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinTest, NoThemeColor) {
  theme_color_ = std::nullopt;
  InstallAndLaunchWebApp();

  EXPECT_EQ(
      frame_view_->GetTitlebarColor(),
      browser()->window()->GetColorProvider()->GetColor(ui::kColorFrameActive));
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinTest, MaximizedLayout) {
  InstallAndLaunchWebApp();
  frame_view_->frame()->Maximize();
  RunScheduledLayouts();

  views::View* const window_title =
      frame_view_->GetViewByID(VIEW_ID_WINDOW_TITLE);
  DCHECK_GT(window_title->x(), 0);
  DCHECK_GE(web_app_frame_toolbar_->y(), 0);
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinTest, RTLTopRightHitTest) {
  base::i18n::SetRTLForTesting(true);
  InstallAndLaunchWebApp();
  RunScheduledLayouts();

  // Avoid the top right resize corner.
  constexpr int kInset = 10;
  EXPECT_EQ(frame_view_->NonClientHitTest(
                gfx::Point(frame_view_->width() - kInset, kInset)),
            HTCAPTION);
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinTest, Fullscreen) {
  InstallAndLaunchWebApp();
  frame_view_->frame()->SetFullscreen(true);
  browser_view_->GetWidget()->LayoutRootViewIfNecessary();

  // Verify that all children except the ClientView are hidden when the window
  // is fullscreened.
  for (views::View* child : frame_view_->children()) {
    EXPECT_EQ(views::IsViewClass<views::ClientView>(child),
              child->GetVisible());
  }
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinTest, ContainerHeight) {
  InstallAndLaunchWebApp();

  static_cast<views::View*>(frame_view_)
      ->GetWidget()
      ->LayoutRootViewIfNecessary();

  EXPECT_EQ(web_app_frame_toolbar_->height(),
            frame_view_->caption_button_container_for_testing()->height());

  frame_view_->frame()->Maximize();

  EXPECT_EQ(web_app_frame_toolbar_->height(),
            frame_view_->caption_button_container_for_testing()->height());
}

class WebAppBrowserFrameViewWinWindowControlsOverlayTest
    : public InProcessBrowserTest {
 public:
  WebAppBrowserFrameViewWinWindowControlsOverlayTest() = default;
  WebAppBrowserFrameViewWinWindowControlsOverlayTest(
      const WebAppBrowserFrameViewWinWindowControlsOverlayTest&) = delete;
  WebAppBrowserFrameViewWinWindowControlsOverlayTest& operator=(
      const WebAppBrowserFrameViewWinWindowControlsOverlayTest&) = delete;

  ~WebAppBrowserFrameViewWinWindowControlsOverlayTest() override = default;

  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    embedded_test_server()->ServeFilesFromDirectory(temp_dir_.GetPath());
    ASSERT_TRUE(embedded_test_server()->Start());
    InProcessBrowserTest::SetUp();
  }

  void InstallAndLaunchWebAppWithWindowControlsOverlay() {
    GURL start_url = web_app_frame_toolbar_helper_
                         .LoadWindowControlsOverlayTestPageWithDataAndGetURL(
                             embedded_test_server(), &temp_dir_);

    std::vector<blink::mojom::DisplayMode> display_overrides = {
        blink::mojom::DisplayMode::kWindowControlsOverlay};
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
    web_app_info->scope = start_url.GetWithoutFilename();
    web_app_info->display_mode = blink::mojom::DisplayMode::kStandalone;
    web_app_info->user_display_mode =
        web_app::mojom::UserDisplayMode::kStandalone;
    web_app_info->title = u"A Web App";
    web_app_info->display_override = display_overrides;

    webapps::AppId app_id = web_app::test::InstallWebApp(
        browser()->profile(), std::move(web_app_info));

    content::TestNavigationObserver navigation_observer(start_url);
    base::RunLoop loop;
    navigation_observer.StartWatchingNewWebContents();
    Browser* app_browser =
        web_app::LaunchWebAppBrowser(browser()->profile(), app_id);

    // TODO(crbug.com/40174440): Register binder for BrowserInterfaceBroker
    // during testing.
    app_browser->app_controller()->SetOnUpdateDraggableRegionForTesting(
        loop.QuitClosure());
    web_app::NavigateViaLinkClickToURLAndWait(app_browser, start_url);
    loop.Run();
    navigation_observer.WaitForNavigationFinished();

    browser_view_ = BrowserView::GetBrowserViewForBrowser(app_browser);
    views::NonClientFrameView* frame_view =
        browser_view_->GetWidget()->non_client_view()->frame_view();

    frame_view_ = static_cast<BrowserFrameViewWin*>(frame_view);
    auto* web_app_frame_toolbar =
        browser_view_->web_app_frame_toolbar_for_testing();

    DCHECK(web_app_frame_toolbar);
    DCHECK(web_app_frame_toolbar->GetVisible());
  }

  void ToggleWindowControlsOverlayEnabledAndWait() {
    auto* web_contents = browser_view_->GetActiveWebContents();
    web_app_frame_toolbar_helper_.SetupGeometryChangeCallback(web_contents);
    base::test::TestFuture<void> future;
    browser_view_->ToggleWindowControlsOverlayEnabled(future.GetCallback());
    EXPECT_TRUE(future.Wait());
    content::TitleWatcher title_watcher(web_contents, u"ongeometrychange");
    std::ignore = title_watcher.WaitAndGetTitle();
  }

  raw_ptr<BrowserView, AcrossTasksDanglingUntriaged> browser_view_ = nullptr;
  raw_ptr<BrowserFrameViewWin, AcrossTasksDanglingUntriaged> frame_view_ =
      nullptr;
  WebAppFrameToolbarTestHelper web_app_frame_toolbar_helper_;

 private:
  web_app::OsIntegrationTestOverrideBlockingRegistration faked_os_integration_;
  base::ScopedTempDir temp_dir_;
};

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinWindowControlsOverlayTest,
                       ContainerHeight) {
  InstallAndLaunchWebAppWithWindowControlsOverlay();
  ToggleWindowControlsOverlayEnabledAndWait();

  EXPECT_EQ(browser_view_->web_app_frame_toolbar_for_testing()->height(),
            frame_view_->caption_button_container_for_testing()->height());

  frame_view_->frame()->Maximize();

  EXPECT_EQ(browser_view_->web_app_frame_toolbar_for_testing()->height(),
            frame_view_->caption_button_container_for_testing()->height());
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinWindowControlsOverlayTest,
                       Fullscreen) {
  InstallAndLaunchWebAppWithWindowControlsOverlay();
  ToggleWindowControlsOverlayEnabledAndWait();

  EXPECT_GT(frame_view_->GetBoundsForClientView().y(), 0);

  frame_view_->frame()->SetFullscreen(true);
  browser_view_->GetWidget()->LayoutRootViewIfNecessary();

  // ClientView should be covering the entire screen.
  EXPECT_EQ(frame_view_->GetBoundsForClientView().y(), 0);

  // Exit full screen.
  frame_view_->frame()->SetFullscreen(false);
}

IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinWindowControlsOverlayTest,
                       CaptionButtonsTooltip) {
  InstallAndLaunchWebAppWithWindowControlsOverlay();
  auto* caption_button_container =
      frame_view_->caption_button_container_for_testing();
  auto* minimize_button = static_cast<const WindowsCaptionButton*>(
      caption_button_container->GetViewByID(VIEW_ID_MINIMIZE_BUTTON));
  auto* maximize_button = static_cast<const WindowsCaptionButton*>(
      caption_button_container->GetViewByID(VIEW_ID_MAXIMIZE_BUTTON));
  auto* restore_button = static_cast<const WindowsCaptionButton*>(
      caption_button_container->GetViewByID(VIEW_ID_RESTORE_BUTTON));
  auto* close_button = static_cast<const WindowsCaptionButton*>(
      caption_button_container->GetViewByID(VIEW_ID_CLOSE_BUTTON));

  // Verify tooltip text was first empty.
  EXPECT_EQ(minimize_button->GetTooltipText(), u"");
  EXPECT_EQ(maximize_button->GetTooltipText(), u"");
  EXPECT_EQ(restore_button->GetTooltipText(), u"");
  EXPECT_EQ(close_button->GetTooltipText(), u"");

  ToggleWindowControlsOverlayEnabledAndWait();

  // Verify tooltip text has been updated.
  EXPECT_EQ(minimize_button->GetTooltipText(),
            minimize_button->GetViewAccessibility().GetCachedName());
  EXPECT_EQ(maximize_button->GetTooltipText(),
            maximize_button->GetViewAccessibility().GetCachedName());
  EXPECT_EQ(restore_button->GetTooltipText(),
            restore_button->GetViewAccessibility().GetCachedName());
  EXPECT_EQ(close_button->GetTooltipText(),
            close_button->GetViewAccessibility().GetCachedName());

  ToggleWindowControlsOverlayEnabledAndWait();

  // Verify tooltip text has been cleared when the feature is toggled off.
  EXPECT_EQ(minimize_button->GetTooltipText(), u"");
  EXPECT_EQ(maximize_button->GetTooltipText(), u"");
  EXPECT_EQ(restore_button->GetTooltipText(), u"");
  EXPECT_EQ(close_button->GetTooltipText(), u"");
}

// TODO(crbug.com/361780162): This test has been flaky on Windows ASan testers.
#if BUILDFLAG(IS_WIN) && defined(ADDRESS_SANITIZER)
#define MAYBE_CaptionButtonHitTest DISABLED_CaptionButtonHitTest
#else
#define MAYBE_CaptionButtonHitTest CaptionButtonHitTest
#endif
IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinWindowControlsOverlayTest,
                       MAYBE_CaptionButtonHitTest) {
  InstallAndLaunchWebAppWithWindowControlsOverlay();
  frame_view_->GetWidget()->LayoutRootViewIfNecessary();

  // Avoid the top right resize corner.
  constexpr int kInset = 10;
  const gfx::Point kPoint(frame_view_->width() - kInset, kInset);

  EXPECT_EQ(frame_view_->NonClientHitTest(kPoint), HTCLOSE);

  ToggleWindowControlsOverlayEnabledAndWait();

  // Verify the component updates on toggle.
  EXPECT_EQ(frame_view_->NonClientHitTest(kPoint), HTCLIENT);

  ToggleWindowControlsOverlayEnabledAndWait();

  // Verify the component clears when the feature is turned off.
  EXPECT_EQ(frame_view_->NonClientHitTest(kPoint), HTCLOSE);
}

// Regression test for https://crbug.com/1286896.
IN_PROC_BROWSER_TEST_F(WebAppBrowserFrameViewWinWindowControlsOverlayTest,
                       TitlebarLayoutAfterUpdateWindowTitle) {
  InstallAndLaunchWebAppWithWindowControlsOverlay();
  ToggleWindowControlsOverlayEnabledAndWait();
  frame_view_->GetWidget()->LayoutRootViewIfNecessary();
  frame_view_->UpdateWindowTitle();

  WebAppFrameToolbarView* web_app_frame_toolbar =
      browser_view_->web_app_frame_toolbar_for_testing();

  // Verify that the center container doesn't consume space by expecting the
  // right container to consume the full width of the WebAppFrameToolbarView.
  EXPECT_EQ(web_app_frame_toolbar->width(),
            web_app_frame_toolbar->get_right_container_for_testing()->width());
}

class WebAppBrowserFrameViewWinWebAppIconInTitlebarTest
    : public WebAppBrowserFrameViewWinTest,
      public testing::WithParamInterface<bool> {
 public:
  WebAppBrowserFrameViewWinWebAppIconInTitlebarTest() {
    if (GetParam()) {
      feature_list_.InitAndEnableFeature(features::kWebAppIconInTitlebar);
    } else {
      feature_list_.InitAndDisableFeature(features::kWebAppIconInTitlebar);
    }
  }
  WebAppBrowserFrameViewWinWebAppIconInTitlebarTest(
      const WebAppBrowserFrameViewWinWebAppIconInTitlebarTest&) = delete;
  WebAppBrowserFrameViewWinWebAppIconInTitlebarTest& operator=(
      const WebAppBrowserFrameViewWinWebAppIconInTitlebarTest&) = delete;

  ~WebAppBrowserFrameViewWinWebAppIconInTitlebarTest() override = default;
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    return info.param ? "Enabled" : "Disabled";
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Verify that the icon is present if the feature is enabled.
IN_PROC_BROWSER_TEST_P(WebAppBrowserFrameViewWinWebAppIconInTitlebarTest,
                       WebAppIconInTitlebar) {
  InstallAndLaunchWebApp();

  ASSERT_EQ(GetParam(), frame_view_->window_icon_for_testing()->GetVisible());
}

INSTANTIATE_TEST_SUITE_P(
    All,
    WebAppBrowserFrameViewWinWebAppIconInTitlebarTest,
    testing::Bool(),
    WebAppBrowserFrameViewWinWebAppIconInTitlebarTest::DescribeParams);