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

#include "chrome/browser/ui/views/frame/browser_frame_view_chromeos.h"

#include <string>

#include "ash/constants/web_app_id_constants.h"
#include "ash/multi_user/multi_user_window_manager.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_menu_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/app_service/app_registry_cache_waiter.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/browser_app_launcher.h"
#include "chrome/browser/ash/boca/on_task/on_task_locked_controller.h"
#include "chrome/browser/ash/login/test/device_state_mixin.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/ash/system_web_apps/test_support/test_system_web_app_installation.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/sessions/session_restore_test_helper.h"
#include "chrome/browser/sessions/session_service_test_helper.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/session/session_controller_client_impl.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/ash/test_util.h"
#include "chrome/browser/ui/bookmarks/bookmark_bar.h"
#include "chrome/browser/ui/bookmarks/bookmark_bar_controller.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#include "chrome/browser/ui/immersive/immersive_mode_controller.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/page_action/page_action_icon_type.h"
#include "chrome/browser/ui/passwords/passwords_client_ui_delegate.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
#include "chrome/browser/ui/views/frame/app_menu_button.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller_chromeos.h"
#include "chrome/browser/ui/views/frame/immersive_mode_tester.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/location_bar/content_setting_image_view.h"
#include "chrome/browser/ui/views/location_bar/custom_tab_bar_view.h"
#include "chrome/browser/ui/views/location_bar/icon_label_bubble_view.h"
#include "chrome/browser/ui/views/location_bar/zoom_bubble_coordinator.h"
#include "chrome/browser/ui/views/location_bar/zoom_bubble_view.h"
#include "chrome/browser/ui/views/page_action/page_action_view.h"
#include "chrome/browser/ui/views/page_action/test_support/page_action_test_support.h"
#include "chrome/browser/ui/views/page_info/page_info_bubble_view_base.h"
#include "chrome/browser/ui/views/profiles/profile_indicator_icon.h"
#include "chrome/browser/ui/views/tab_search_bubble_host.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/toolbar/app_menu.h"
#include "chrome/browser/ui/views/toolbar/app_menu_control.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.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_menu_button.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/test/prevent_close_test_base.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/web_applications/web_app_provider.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/caption_buttons/frame_size_button.h"
#include "chromeos/ui/frame/default_frame_header.h"
#include "chromeos/ui/frame/frame_header.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller_test_api.h"
#include "chromeos/ui/frame/multitask_menu/float_controller_base.h"
#include "components/account_id/account_id.h"
#include "components/account_id/account_id_literal.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/services/app_service/public/cpp/app_launch_params.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/session_manager/core/session.h"
#include "components/session_manager/core/session_manager.h"
#include "components/signin/public/identity_manager/account_managed_status_finder.h"
#include "components/user_manager/user_manager_pref_names.h"
#include "components/zoom/zoom_controller.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/test/background_color_change_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/base/class_property.h"
#include "ui/base/hit_test.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/compositor/layer_solid_color.h"
#include "ui/display/screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/native_theme/mock_os_settings_provider.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/caption_button_layout_constants.h"
#include "ui/views/window/frame_caption_button.h"

using BrowserFrameViewChromeOSTest =
    TopChromeMdParamTest<ChromeOSBrowserUITest>;


class BrowserFrameViewChromeOSTestApi {
 public:
  explicit BrowserFrameViewChromeOSTestApi(BrowserFrameViewChromeOS* frame_view)
      : frame_view_(frame_view) {}
  BrowserFrameViewChromeOSTestApi(const BrowserFrameViewChromeOSTestApi&) =
      delete;
  BrowserFrameViewChromeOSTestApi& operator=(
      const BrowserFrameViewChromeOSTestApi&) = delete;
  ~BrowserFrameViewChromeOSTestApi() = default;

  bool GetShouldPaint() const { return frame_view_->GetShouldPaint(); }

  ProfileIndicatorIcon* GetProfileIndicatorIcon() {
    return frame_view_->profile_indicator_icon_;
  }

  chromeos::FrameHeader* GetFrameHeader() {
    return frame_view_->frame_header_.get();
  }

 private:
  const raw_ptr<BrowserFrameViewChromeOS> frame_view_;
};

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest, NonClientHitTest) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::Widget* widget = browser_view->GetWidget();
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  // Click on the top edge of a restored window hits the top edge resize handle.
  const int kWindowWidth = 300;
  const int kWindowHeight = 290;
  widget->SetBounds(gfx::Rect(10, 10, kWindowWidth, kWindowHeight));
  gfx::Point top_edge(kWindowWidth / 2, 0);
  EXPECT_EQ(HTTOP, frame_view->NonClientHitTest(top_edge));

  // Click just below the resize handle hits the caption.
  gfx::Point below_resize(kWindowWidth / 2, chromeos::kResizeInsideBoundsSize);
  EXPECT_EQ(HTCAPTION, frame_view->NonClientHitTest(below_resize));

  // Click in the top edge of a maximized window now hits the client area,
  // because we want it to fall through to the tab strip and select a tab.
  {
    gfx::Rect old_bounds = frame_view->bounds();
    widget->Maximize();
    auto* window = widget->GetNativeWindow();
    EXPECT_EQ(chromeos::WindowStateType::kMaximized,
              window->GetProperty(chromeos::kWindowStateTypeKey));
    EXPECT_NE(old_bounds, frame_view->bounds());
  }
  EXPECT_EQ(HTCLIENT, frame_view->NonClientHitTest(top_edge));
}

// Tests that caption buttons match tabstrip preferred height in restored,
// maximized, and immersive fullscreen states.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       CaptionButtonHeightInRestoredMaximizedAndFullscreen) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::Widget* widget = browser_view->GetWidget();
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  const int tabstrip_height =
      browser_view->GetFrameElementInfo().tabstrip_preferred_height;
  ASSERT_GT(tabstrip_height, 0);

  // In restored state, caption button container height is the max of the
  // tabstrip height and the default restored button layout height.
  widget->Restore();
  views::test::RunScheduledLayout(widget);
  const int expected_restored_height =
      std::max(tabstrip_height,
               views::GetCaptionButtonLayoutSize(
                   views::CaptionButtonLayoutSize::kBrowserCaptionRestored)
                   .height());
  EXPECT_EQ(expected_restored_height,
            frame_view->caption_button_container()->bounds().height());

  // In restored state, buttons should be vertically centered within the
  // restored container height.
  const int expected_restored_center_y = expected_restored_height / 2;
  for (views::View* button :
       frame_view->caption_button_container()->children()) {
    if (button->GetVisible()) {
      EXPECT_NEAR(expected_restored_center_y,
                  button->bounds().CenterPoint().y(), 1);
    }
  }

  // In maximized state, caption button container height should stretch to match
  // the tabstrip height (stretched to fill the header rather than 34px).
  widget->Maximize();
  views::test::RunScheduledLayout(widget);
  EXPECT_EQ(tabstrip_height,
            frame_view->caption_button_container()->bounds().height());

  // In maximized state, buttons should be vertically centered within the
  // tabstrip / header height.
  const int expected_center_y = tabstrip_height / 2;
  for (views::View* button :
       frame_view->caption_button_container()->children()) {
    if (button->GetVisible()) {
      EXPECT_NEAR(expected_center_y, button->bounds().CenterPoint().y(), 1);
    }
  }

  // In immersive fullscreen state, caption button container height should
  // match the tabstrip height both before and during revealed state, and
  // buttons should be centered.
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  auto tester = std::make_unique<ImmersiveModeTester>(browser());
  EnterImmersiveFullscreenMode(browser());
  tester->WaitForRevealStarted();
  tester->WaitForRevealEnded();
  tester.reset();

  // Before revealed state: immersive frame is not revealed.
  EXPECT_FALSE(immersive_mode_controller->IsRevealed());
  views::test::RunScheduledLayout(widget);
  EXPECT_EQ(tabstrip_height,
            frame_view->caption_button_container()->bounds().height());
  for (views::View* button :
       frame_view->caption_button_container()->children()) {
    if (button->GetVisible()) {
      EXPECT_NEAR(expected_center_y, button->bounds().CenterPoint().y(), 1);
    }
  }

  // During revealed state: immersive frame is revealed.
  std::unique_ptr<ImmersiveRevealedLock> revealed_lock =
      immersive_mode_controller->GetRevealedLock(
          ImmersiveModeController::ANIMATE_REVEAL_NO);
  EXPECT_TRUE(immersive_mode_controller->IsRevealed());
  views::test::RunScheduledLayout(widget);
  EXPECT_EQ(tabstrip_height,
            frame_view->caption_button_container()->bounds().height());
  for (views::View* button :
       frame_view->caption_button_container()->children()) {
    if (button->GetVisible()) {
      EXPECT_NEAR(expected_center_y, button->bounds().CenterPoint().y(), 1);
    }
  }
  revealed_lock.reset();
  ExitImmersiveFullscreenMode(browser());
}

// Regression test for crbug.com/40945061. Asserts that the content window
// accepts input from the edge of the browser frame when the browser is
// maximized.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       ContentWindowAcceptsEdgeInputsWhenMaximized) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  content::WebContents* web_contents = browser_view->GetActiveWebContents();
  views::Widget* widget = browser_view->GetWidget();
  const BrowserFrameViewChromeOS* frame_view =
      GetFrameViewChromeOS(browser_view);

  // Maximize the widget.
  EXPECT_FALSE(widget->IsMaximized());
  const gfx::Rect old_bounds = frame_view->bounds();
  widget->Maximize();
  auto* window = widget->GetNativeWindow();
  EXPECT_EQ(chromeos::WindowStateType::kMaximized,
            window->GetProperty(chromeos::kWindowStateTypeKey));
  EXPECT_NE(old_bounds, frame_view->bounds());

  // Assert that input events at the edge of the browser are propagated to the
  // web contents window.
  EXPECT_FALSE(web_contents->GetFocusedFrame());
  ui::test::EventGenerator event_generator(window->GetRootWindow());
  ASSERT_NO_FATAL_FAILURE(
      event_generator.GestureTapAt(frame_view->bounds().left_center()));
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return !!web_contents->GetFocusedFrame(); }));
}

using BrowserFrameViewChromeOSTouchTest =
    TopChromeTouchTest<ChromeOSBrowserUITest>;


// Test that the frame view does not do any painting in non-immersive
// fullscreen.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest, NonImmersiveFullscreen) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  content::WebContents* web_contents = browser_view->GetActiveWebContents();
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  BrowserFrameViewChromeOSTestApi test_api(frame_view);

  // Frame paints by default.
  EXPECT_TRUE(test_api.GetShouldPaint());

  // No painting should occur in non-immersive fullscreen. (We enter into tab
  // fullscreen here because tab fullscreen is non-immersive even on ChromeOS).
  EnterTabFullscreenMode(browser(), web_contents);
  EXPECT_FALSE(ImmersiveModeController::From(browser())->IsEnabled());
  EXPECT_TRUE(browser_view->IsFullscreen());
  EXPECT_FALSE(test_api.GetShouldPaint());

  // The client view abuts top of the window.
  EXPECT_EQ(0, frame_view->GetBoundsForClientView().y());

  // The frame should be painted again when fullscreen is exited and the caption
  // buttons should be visible.
  ExitTabFullscreenMode(browser(), web_contents);
  EXPECT_FALSE(ImmersiveModeController::From(browser())->IsEnabled());
  EXPECT_FALSE(browser_view->IsFullscreen());
  EXPECT_TRUE(test_api.GetShouldPaint());
}

// Tests that caption buttons are hidden when entering tab fullscreen.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       CaptionButtonsHiddenNonImmersiveFullscreen) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  content::WebContents* web_contents = browser_view->GetActiveWebContents();
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  EnterTabFullscreenMode(browser(), web_contents);
  EXPECT_TRUE(browser_view->IsFullscreen());
  EXPECT_FALSE(ImmersiveModeController::From(browser())->IsEnabled());
  // Caption buttons are hidden.
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  // The frame should be painted again when fullscreen is exited and the caption
  // buttons should be visible.
  ExitTabFullscreenMode(browser(), web_contents);
  EXPECT_FALSE(ImmersiveModeController::From(browser())->IsEnabled());
  EXPECT_FALSE(browser_view->IsFullscreen());
  // Caption button container visible again.
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       IncognitoMarkedAsAssistantBlocked) {
  Browser* incognito_browser = CreateIncognitoBrowser();
  EXPECT_TRUE(incognito_browser->GetWindow()->GetNativeWindow()->GetProperty(
      chromeos::kBlockedForAssistantSnapshotKey));
}

// Tests that browser frame minimum size constraint is updated in response to
// browser view layout.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest, FrameMinSizeIsUpdated) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  BookmarkBarView* bookmark_bar = browser_view->GetBookmarkBarView();
  EXPECT_FALSE(bookmark_bar->GetVisible());
  const int min_height_no_bookmarks = frame_view->GetMinimumSize().height();

  // Setting non-zero bookmark bar preferred size forces it to be visible and
  // triggers BrowserView layout update.
  bookmark_bar->SetPreferredSize(gfx::Size(50, 5));
  browser_view->GetWidget()->LayoutRootViewIfNecessary();
  EXPECT_TRUE(bookmark_bar->GetVisible());

  // Minimum window size should grow with the bookmark bar shown.
  gfx::Size min_window_size = frame_view->GetMinimumSize();
  EXPECT_GT(min_window_size.height(), min_height_no_bookmarks);
}

// This is a regression test that session restore minimized browser should
// re-layout the header (https://crbug.com/40569439).
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       RestoreMinimizedBrowserUpdatesCaption) {
  // Enable session service.
  SessionStartupPref pref(SessionStartupPref::LAST);
  Profile* profile = browser()->GetProfile();
  SessionStartupPref::SetStartupPref(profile, pref);

  SessionServiceTestHelper helper(profile);
  helper.SetForceBrowserNotAliveWithNoWindows(true);

  // Do not exit from test when last browser is closed.
  ScopedKeepAlive keep_alive(KeepAliveOrigin::SESSION_RESTORE,
                             KeepAliveRestartOption::DISABLED);

  // Quit and restore.
  browser()->GetWindow()->Minimize();
  CloseBrowserSynchronously(browser());

  chrome::NewEmptyWindow(profile);
  SessionRestoreTestHelper().Wait();

  const BrowserWindowInterface* const new_browser =
      GetLastActiveBrowserWindowInterfaceWithAnyProfile();

  // Check that a layout occurs.
  BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(new_browser);
  views::Widget* widget = browser_view->GetWidget();

  BrowserFrameViewChromeOS* frame_view = static_cast<BrowserFrameViewChromeOS*>(
      widget->non_client_view()->frame_view());

  chromeos::FrameCaptionButtonContainerView::TestApi test(
      frame_view->caption_button_container());
  EXPECT_TRUE(test.size_button()->icon_definition_for_test());
}

namespace {

class WebAppFrameViewChromeOSTest
    : public TopChromeMdParamTest<ChromeOSBrowserUITest> {
 public:
  WebAppFrameViewChromeOSTest() {
    scoped_feature_list_.InitAndEnableFeature(::features::kWebAppInstallDialog);
  }
  WebAppFrameViewChromeOSTest(const WebAppFrameViewChromeOSTest&) = delete;
  WebAppFrameViewChromeOSTest& operator=(const WebAppFrameViewChromeOSTest&) =
      delete;
  ~WebAppFrameViewChromeOSTest() override = default;

  GURL GetAppURL() const {
    return https_server_.GetURL("app.com", "/ssl/google.html");
  }

  static SkColor GetThemeColor() { return SK_ColorBLUE; }

  raw_ptr<BrowserWindowInterface, DanglingUntriaged> app_browser_ = nullptr;
  raw_ptr<BrowserView, DanglingUntriaged> browser_view_ = nullptr;
  raw_ptr<chromeos::DefaultFrameHeader, DanglingUntriaged> frame_header_ =
      nullptr;
  raw_ptr<WebAppFrameToolbarView, DanglingUntriaged> web_app_frame_toolbar_ =
      nullptr;
  raw_ptr<
      const std::vector<raw_ptr<ContentSettingImageView, VectorExperimental>>,
      DanglingUntriaged>
      content_setting_views_ = nullptr;
  raw_ptr<AppMenuButton, DanglingUntriaged> web_app_menu_button_ = nullptr;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    TopChromeMdParamTest<ChromeOSBrowserUITest>::SetUpCommandLine(command_line);
    cert_verifier_.SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    TopChromeMdParamTest<
        ChromeOSBrowserUITest>::SetUpInProcessBrowserTestFixture();
    cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    cert_verifier_.TearDownInProcessBrowserTestFixture();
    TopChromeMdParamTest<
        ChromeOSBrowserUITest>::TearDownInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    TopChromeMdParamTest<ChromeOSBrowserUITest>::SetUpOnMainThread();

    WebAppToolbarButtonContainer::DisableAnimationForTesting(true);

    // Start secure local server.
    host_resolver()->AddRule("*", "127.0.0.1");
    cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
    ASSERT_TRUE(https_server_.Start());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  // |SetUpWebApp()| must be called after |SetUpOnMainThread()| to make sure
  // the Network Service process has been setup properly.
  void SetUpWebApp() {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(GetAppURL());
    web_app_info->scope = GetAppURL().GetWithoutFilename();
    web_app_info->display_mode = blink::mojom::DisplayMode::kStandalone;
    web_app_info->theme_color = GetThemeColor();

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

    browser_view_ = BrowserView::GetBrowserViewForBrowser(app_browser_);
    BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view_);
    frame_header_ = static_cast<chromeos::DefaultFrameHeader*>(
        BrowserFrameViewChromeOSTestApi(frame_view).GetFrameHeader());

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

    content_setting_views_ =
        &web_app_frame_toolbar_->GetContentSettingViewsForTesting();
    web_app_menu_button_ = views::AsViewClass<AppMenuButton>(
        views::ElementTrackerViews::GetInstance()->GetFirstMatchingView(
            kToolbarAppMenuButtonElementId,
            views::ElementTrackerViews::GetContextForView(browser_view_)));
  }

  BrowserView* CreateWebAppPopup(BrowserWindowInterface* parent_browser) {
    NavigateParams navigate_params(parent_browser, GetAppURL(),
                                   ui::PAGE_TRANSITION_LINK);
    navigate_params.disposition = WindowOpenDisposition::NEW_POPUP;

    content::TestNavigationObserver navigation_observer(GetAppURL());
    navigation_observer.StartWatchingNewWebContents();
    Navigate(&navigate_params);
    navigation_observer.WaitForNavigationFinished();

    return BrowserView::GetBrowserViewForBrowser(navigate_params.browser);
  }

  AppMenu* GetAppMenu() { return web_app_menu_button_->app_menu(); }

  SkColor GetActiveColor() const {
    return *web_app_frame_toolbar_->active_foreground_color_;
  }

  bool GetPaintingAsActive() const {
    return web_app_frame_toolbar_->paint_as_active_;
  }

  IconLabelBubbleView* GetPageActionView(actions::ActionId action_id) {
    auto* provider = browser_view_->toolbar_button_provider();
    return page_actions::GetIconLabelBubbleViewForTesting(
        provider->GetPageActionViewInterface(action_id), action_id);
  }

  ContentSettingImageView* GrantGeolocationPermission() {
    content::RenderFrameHost* frame = app_browser_->GetTabStripModel()
                                          ->GetActiveWebContents()
                                          ->GetPrimaryMainFrame();
    content_settings::PageSpecificContentSettings* content_settings =
        content_settings::PageSpecificContentSettings::GetForFrame(frame);
    content_settings->OnContentAllowed(ContentSettingsType::GEOLOCATION);

    return *std::ranges::find(*content_setting_views_,
                              ContentSettingImageModel::ImageType::kGeolocation,
                              &ContentSettingImageView::GetType);
  }

  void SimulateClickOnView(views::View* view) {
    const gfx::Point point;
    ui::MouseEvent event(ui::EventType::kMousePressed, point, point,
                         ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                         ui::EF_LEFT_MOUSE_BUTTON);
    view->OnMouseEvent(&event);
    ui::MouseEvent event_rel(ui::EventType::kMouseReleased, point, point,
                             ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                             ui::EF_LEFT_MOUSE_BUTTON);
    view->OnMouseEvent(&event_rel);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  // For mocking a secure site.
  net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
  content::ContentMockCertVerifier cert_verifier_;
};

}  // namespace

// Tests that the page info dialog doesn't anchor in a way that puts it outside
// of web-app windows. This is important as some platforms don't support bubble
// anchor adjustment (see |BubbleDialogDelegateView::CreateBubble()|).
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, PageInfoBubblePosition) {
  SetUpWebApp();

  // Resize app window to only take up the left half of the screen.
  views::Widget* widget = browser_view_->GetWidget();
  gfx::Size screen_size =
      display::Screen::Get()
          ->GetDisplayNearestWindow(widget->GetNativeWindow())
          .work_area_size();
  widget->SetBounds(
      gfx::Rect(0, 0, screen_size.width() / 2, screen_size.height()));

  // Show page info dialog (currently PWAs use page info in place of an actual
  // app info dialog).
  chrome::ExecuteCommand(app_browser_, IDC_WEB_APP_MENU_APP_INFO);

  // Check the bubble anchors inside the main app window even if there's space
  // available outside the main app window.
  gfx::Rect page_info_bounds =
      PageInfoBubbleViewBase::GetPageInfoBubbleForTesting()
          ->GetWidget()
          ->GetWindowBoundsInScreen();
  EXPECT_TRUE(widget->GetWindowBoundsInScreen().Contains(page_info_bounds));
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, FocusableViews) {
  SetUpWebApp();
  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());
  browser_view_->GetFocusManager()->AdvanceFocus(false);
  EXPECT_TRUE(web_app_menu_button_->HasFocus());
  browser_view_->GetFocusManager()->AdvanceFocus(false);
  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest,
                       ButtonVisibilityInOverviewMode) {
  SetUpWebApp();
  EXPECT_TRUE(web_app_frame_toolbar_->GetVisible());

  EnterOverviewMode();
  views::test::RunScheduledLayout(browser_view_);
  EXPECT_FALSE(web_app_frame_toolbar_->GetVisible());

  ExitOverviewMode();
  views::test::RunScheduledLayout(browser_view_);
  EXPECT_TRUE(web_app_frame_toolbar_->GetVisible());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, FrameThemeColorIsSet) {
  SetUpWebApp();
  aura::Window* window = browser_view_->GetWidget()->GetNativeWindow();
  EXPECT_EQ(GetThemeColor(),
            window->GetProperty(chromeos::kFrameActiveColorKey));
  EXPECT_EQ(GetThemeColor(),
            window->GetProperty(chromeos::kFrameInactiveColorKey));
  EXPECT_EQ(gfx::kGoogleGrey200, GetActiveColor());
}

// Make sure that for web apps, the height of the frame doesn't exceed the
// height of the caption buttons.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, FrameSize) {
  SetUpWebApp();
  const int inset = GetFrameViewChromeOS(browser_view_)->GetTopInset(false);
  EXPECT_EQ(inset, views::GetCaptionButtonLayoutSize(
                       views::CaptionButtonLayoutSize::kNonBrowserCaption)
                       .height());
  EXPECT_GE(inset, web_app_menu_button_->size().height());
  EXPECT_GE(inset, web_app_frame_toolbar_->size().height());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, IsToolbarButtonProvider) {
  SetUpWebApp();
  EXPECT_EQ(browser_view_->toolbar_button_provider(), web_app_frame_toolbar_);
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, ShowManagePasswordsIcon) {
  SetUpWebApp();
  content::WebContents* web_contents =
      app_browser_->GetTabStripModel()->GetActiveWebContents();
  IconLabelBubbleView* manage_passwords_icon =
      GetPageActionView(kActionShowPasswordsBubbleOrPage);

  EXPECT_TRUE(manage_passwords_icon);
  EXPECT_FALSE(manage_passwords_icon->GetVisible());

  password_manager::StoredCredential credential;
  credential.username_value = u"test";
  credential.url = GetAppURL().DeprecatedGetOriginAsURL();
  credential.match_type = password_manager::PasswordForm::MatchType::kExact;
  std::vector<password_manager::StoredCredential> credentials;
  credentials.push_back(std::move(credential));
  PasswordsClientUIDelegateFromWebContents(web_contents)
      ->OnPasswordAutofilled(credentials,
                             url::Origin::Create(credentials[0].url), {});
  chrome::ManagePasswordsForPage(app_browser_);
  EXPECT_TRUE(manage_passwords_icon->GetVisible());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, ShowZoomIcon) {
  SetUpWebApp();
  content::WebContents* web_contents =
      app_browser_->GetTabStripModel()->GetActiveWebContents();
  zoom::ZoomController* zoom_controller =
      zoom::ZoomController::FromWebContents(web_contents);
  IconLabelBubbleView* zoom_icon = GetPageActionView(kActionShowZoomBubble);

  ZoomBubbleCoordinator* zoom_bubble_coordinator =
      ZoomBubbleCoordinator::From(app_browser_);

  EXPECT_TRUE(zoom_icon);
  EXPECT_FALSE(zoom_icon->GetVisible());
  EXPECT_FALSE(zoom_bubble_coordinator->bubble());

  zoom_controller->SetZoomLevel(blink::ZoomFactorToZoomLevel(1.5));
  EXPECT_TRUE(zoom_icon->GetVisible());
  EXPECT_TRUE(zoom_bubble_coordinator->bubble());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, ShowFindIcon) {
  SetUpWebApp();

  IconLabelBubbleView* find_icon = GetPageActionView(kActionFind);
  ASSERT_TRUE(find_icon);
  EXPECT_FALSE(find_icon->GetVisible());

  chrome::Find(app_browser_);
  EXPECT_TRUE(find_icon->GetVisible());
}

// TODO(crbug.com/420040505): Fix failures on the Linux Chromium OS ASan LSan
// Tests bot. (Actually this is flaky on *all* ChromeOS builds.)
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_ShowTranslateIcon DISABLED_ShowTranslateIcon
#else
#define MAYBE_ShowTranslateIcon ShowTranslateIcon
#endif
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, MAYBE_ShowTranslateIcon) {
  SetUpWebApp();
  IconLabelBubbleView* translate_icon = GetPageActionView(kActionShowTranslate);

  ASSERT_TRUE(translate_icon);
  EXPECT_FALSE(translate_icon->GetVisible());

  chrome::Find(app_browser_);
  browser_view_->ShowTranslateBubble(browser_view_->GetActiveWebContents(),
                                     translate::TRANSLATE_STEP_AFTER_TRANSLATE,
                                     "en", "fr",
                                     translate::TranslateErrors::NONE, true);

  EXPECT_TRUE(translate_icon->GetVisible());
}

// Tests that the focus toolbar command focuses the app menu button in web-app
// windows.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest,
                       BrowserCommandFocusToolbarAppMenu) {
  SetUpWebApp();
  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());

  EXPECT_FALSE(web_app_menu_button_->HasFocus());
  chrome::ExecuteCommand(app_browser_, IDC_FOCUS_TOOLBAR);
  EXPECT_TRUE(web_app_menu_button_->HasFocus());
}

// Tests that the focus toolbar command focuses content settings icons before
// the app menu button when present in web-app windows.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest,
                       BrowserCommandFocusToolbarGeolocation) {
  SetUpWebApp();

  ContentSettingImageView* geolocation_icon = GrantGeolocationPermission();

  // In order to receive focus, the geo icon must be laid out (and be both
  // visible and nonzero size).
  RunScheduledLayouts();

  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());
  EXPECT_FALSE(web_app_menu_button_->HasFocus());
  EXPECT_FALSE(geolocation_icon->HasFocus());

  chrome::ExecuteCommand(app_browser_, IDC_FOCUS_TOOLBAR);

  EXPECT_TRUE(geolocation_icon->HasFocus());
  EXPECT_FALSE(web_app_menu_button_->HasFocus());
}

// Tests that the show app menu command opens the app menu for web-app windows.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, BrowserCommandShowAppMenu) {
  SetUpWebApp();
  EXPECT_EQ(nullptr, GetAppMenu());
  chrome::ExecuteCommand(app_browser_, IDC_SHOW_APP_MENU);
  EXPECT_NE(nullptr, GetAppMenu());
}

// Tests that the focus next pane command focuses the app menu for web-app
// windows.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest,
                       BrowserCommandFocusNextPane) {
  SetUpWebApp();
  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());
  EXPECT_FALSE(web_app_menu_button_->HasFocus());
  chrome::ExecuteCommand(app_browser_, IDC_FOCUS_NEXT_PANE);
  EXPECT_TRUE(web_app_menu_button_->HasFocus());
}

// Tests the app icon is not shown but the title is shown.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, IconNotShownButTitleShown) {
  SetUpWebApp();
  auto* browser_view = BrowserView::GetBrowserViewForBrowser(app_browser_);
  EXPECT_FALSE(browser_view->ShouldShowWindowIcon());
  EXPECT_TRUE(browser_view->ShouldShowWindowTitle());
}

// Tests that the custom tab bar is focusable from the keyboard.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, CustomTabBarIsFocusable) {
  SetUpWebApp();
  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());

  auto* browser_view = BrowserView::GetBrowserViewForBrowser(app_browser_);

  const GURL kOutOfScopeURL("http://example.org/");
  NavigateParams nav_params(app_browser_, kOutOfScopeURL,
                            ui::PAGE_TRANSITION_LINK);
  ui_test_utils::NavigateToURL(&nav_params);
  auto* custom_tab_bar = browser_view->toolbar()->custom_tab_bar();

  chrome::ExecuteCommand(app_browser_, IDC_FOCUS_NEXT_PANE);
  EXPECT_TRUE(web_app_menu_button_->HasFocus());

  EXPECT_FALSE(custom_tab_bar->close_button_for_testing()->HasFocus());
  chrome::ExecuteCommand(app_browser_, IDC_FOCUS_NEXT_PANE);
  EXPECT_TRUE(custom_tab_bar->close_button_for_testing()->HasFocus());
}

// Tests that the focus previous pane command focuses the app menu for web-app
// windows.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest,
                       BrowserCommandFocusPreviousPane) {
  SetUpWebApp();
  EXPECT_TRUE(browser_view_->contents_web_view()->HasFocus());
  EXPECT_FALSE(web_app_menu_button_->HasFocus());
  chrome::ExecuteCommand(app_browser_, IDC_FOCUS_PREVIOUS_PANE);
  EXPECT_TRUE(web_app_menu_button_->HasFocus());
}

// Tests that a web app's content settings icons can be interacted with.
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, ContentSettingIcons) {
  SetUpWebApp();
  for (ContentSettingImageView* view : *content_setting_views_) {
    EXPECT_FALSE(view->GetVisible());
  }

  ContentSettingImageView* geolocation_icon = GrantGeolocationPermission();

  for (ContentSettingImageView* view : *content_setting_views_) {
    bool is_geolocation_icon = view == geolocation_icon;
    EXPECT_EQ(is_geolocation_icon, view->GetVisible());
  }
  EXPECT_FALSE(geolocation_icon->IsBubbleShowing());

  // Press the geolocation button.
  geolocation_icon->OnKeyPressed(
      ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_SPACE, ui::EF_NONE));
  geolocation_icon->OnKeyReleased(
      ui::KeyEvent(ui::EventType::kKeyReleased, ui::VKEY_SPACE, ui::EF_NONE));
  EXPECT_TRUE(geolocation_icon->IsBubbleShowing());
}

// Regression test for https://crbug.com/40574975
IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest,
                       ActiveStateOfButtonMatchesWidget) {
  SetUpWebApp();
  chromeos::FrameCaptionButtonContainerView::TestApi test(
      GetFrameViewChromeOS(browser_view_)->caption_button_container());

  EXPECT_TRUE(test.size_button()->GetPaintAsActive());
  EXPECT_TRUE(GetPaintingAsActive());

  DeactivateWidget(browser_view_->GetWidget());
  EXPECT_FALSE(test.size_button()->GetPaintAsActive());
  EXPECT_FALSE(GetPaintingAsActive());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, PopupHasToolbar) {
  SetUpWebApp();
  BrowserView* browser_view = CreateWebAppPopup(app_browser_);
  EXPECT_TRUE(browser_view->web_app_frame_toolbar_for_testing() &&
              browser_view->web_app_frame_toolbar_for_testing()->GetVisible());
}

IN_PROC_BROWSER_TEST_P(WebAppFrameViewChromeOSTest, PopupFullscreenNoTopInset) {
  SetUpWebApp();
  BrowserView* browser_view = CreateWebAppPopup(app_browser_);
  content::WebContents* web_contents = browser_view->GetActiveWebContents();
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  EnterTabFullscreenMode(browser_view->browser(), web_contents);
  EXPECT_TRUE(browser_view->IsFullscreen());

  const auto layout_params = frame_view->GetBrowserLayoutParams();
  EXPECT_EQ(0.0f, layout_params.leading_exclusion.content.height());
  EXPECT_EQ(0, frame_view->GetBoundsForClientView().y());
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       ShortcutRevealTopChromeExceptForZoom) {
  // Make sure bookmark bar is visible.
  chrome::ToggleBookmarkBar(browser());

  // Create a bookmark that will be focused.
  bookmarks::BookmarkModel* bookmark_model =
      BookmarkModelFactory::GetForBrowserContext(browser()->GetProfile());
  const bookmarks::BookmarkNode* bookmark_bar_node =
      bookmark_model->bookmark_bar_node();
  bookmark_model->AddURL(bookmark_bar_node, 0, u"Test Bookmark",
                         GURL("https://www.example.com"));

  // Key events are sent to web contents first then processed as accelerator
  // when they're not handled by them.  Make sure that web contents is ready to
  // make sure that test covers production scenario.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")));
  ASSERT_TRUE(content::WaitForLoadStop(
      browser()->GetTabStripModel()->GetActiveWebContents()));

  EnterImmersiveFullscreenMode(browser());

  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());

  // Top chrome is initially revealed when entiring fullscreen. Mak sure
  // the top chrome is hidden after timeout.
  // TODO(crbug.com/463559714): Replace the loop with EXPECT_TRUE, when the
  // mechanism to disable gfx::Animation is added.
  EXPECT_TRUE(immersive_mode_controller->IsRevealed());
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return !immersive_mode_controller->IsRevealed(); }));

  chromeos::ImmersiveFullscreenControllerTestApi test_api(
      static_cast<ImmersiveModeControllerChromeos*>(
          ImmersiveModeController::From(browser()))
          ->controller());
  EXPECT_FALSE(test_api.IsRevealLocked());

  enum ShortcutType {
    kFocusToolbar,
    kFocusOmnibox,
    kFocusBookmarkBar,
    kCreateBookmark,
    kFocusMultitaskMenu
  };

  for (auto shortcut : {
           kFocusToolbar,
           kFocusOmnibox,
           kFocusBookmarkBar,
           kCreateBookmark,
           kFocusMultitaskMenu,
       }) {
    EXPECT_TRUE(immersive_mode_controller->IsEnabled());
    ui::test::EventGenerator generator(
        browser()->GetWindow()->GetNativeWindow()->GetRootWindow());

    std::string trace_name;
    switch (shortcut) {
      case kFocusOmnibox: {
        trace_name = "FocusOmnibox";
        // Ctrl-L focuses the omnibox.
        generator.PressKey(ui::KeyboardCode::VKEY_CONTROL, 0);
        generator.PressKey(ui::KeyboardCode::VKEY_L, ui::EF_CONTROL_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_L, ui::EF_CONTROL_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_CONTROL, 0);
      } break;
      case kFocusToolbar: {
        trace_name = "FocusToolbar";
        // Shift-Alt-T focuses the first item on toolbar.
        generator.PressKey(ui::KeyboardCode::VKEY_SHIFT, 0);
        generator.PressKey(ui::KeyboardCode::VKEY_MENU, ui::EF_SHIFT_DOWN);
        generator.PressKey(ui::KeyboardCode::VKEY_T,
                           ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_T,
                             ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_MENU, ui::EF_SHIFT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_SHIFT, 0);
      } break;
      case kFocusBookmarkBar: {
        trace_name = "FocusBookmarkBar";
        // Shift-Alt-B focues bookmark bar.

        // Ensure bookmark bar is enabled and visible.
        ASSERT_EQ(BookmarkBarController::From(browser())->bookmark_bar_state(),
                  BookmarkBar::SHOW);
        EXPECT_EQ(bookmark_bar_node->children().size(), 1u);

        generator.PressKey(ui::KeyboardCode::VKEY_SHIFT, 0);
        generator.PressKey(ui::KeyboardCode::VKEY_MENU, ui::EF_SHIFT_DOWN);
        generator.PressKey(ui::KeyboardCode::VKEY_B,
                           ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_B,
                             ui::EF_SHIFT_DOWN | ui::EF_ALT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_MENU, ui::EF_SHIFT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_SHIFT, 0);
      } break;
      case kCreateBookmark: {
        // Alt-D trigger "create bookmark" flow which focues omnibox.
        trace_name = "CreateBookmark";
        generator.PressKey(ui::KeyboardCode::VKEY_MENU, 0);
        generator.PressKey(ui::KeyboardCode::VKEY_D, ui::EF_ALT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_D, ui::EF_ALT_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_MENU, 0);
      } break;
      case kFocusMultitaskMenu: {
        // The multask menu is not accessible via shortcut in in Tablet mode.
        if (GetParam()) {
          continue;
        }
        trace_name = "FocusMultitaskMenu";
        generator.PressKey(ui::KeyboardCode::VKEY_COMMAND, 0);
        generator.PressKey(ui::KeyboardCode::VKEY_Z, ui::EF_COMMAND_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_Z, ui::EF_COMMAND_DOWN);
        generator.ReleaseKey(ui::KeyboardCode::VKEY_COMMAND, 0);
      } break;
    }
    SCOPED_TRACE(trace_name);

    // We need to wait here because the keysequence will be handled upon
    // unhandled event from renderer.
    ASSERT_TRUE(base::test::RunUntil(
        [&]() { return immersive_mode_controller->IsRevealed(); }));
    EXPECT_TRUE(test_api.IsRevealLocked());

    generator.MoveMouseToCenterOf(browser()->GetWindow()->GetNativeWindow());
    generator.ClickLeftButton();

    // TODO(crbug.com/463559714): Replace the loop with EXPECT_TRUE, when the
    // mechanism to disable gfx::Animation is added.
    ASSERT_TRUE(base::test::RunUntil(
        [&]() { return !immersive_mode_controller->IsRevealed(); }));
    EXPECT_TRUE(!test_api.IsRevealLocked());
  }
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       ZoomShortcutShouldNotRevealTopChrome) {
  EnterImmersiveFullscreenMode(browser());

  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());
  // TODO(crbug.com/463559714): Replace the loop with EXPECT_TRUE, when the
  // mechanism to disable gfx::Animation is added.
  ASSERT_TRUE(base::test::RunUntil(
      [&]() -> bool { return !immersive_mode_controller->IsRevealed(); }));

  ui::test::EventGenerator generator(
      browser()->GetWindow()->GetNativeWindow()->GetRootWindow());

  SCOPED_TRACE("Zoom");
  generator.PressKey(ui::KeyboardCode::VKEY_CONTROL, 0);
  generator.PressKey(ui::KeyboardCode::VKEY_OEM_PLUS, ui::EF_CONTROL_DOWN);
  generator.ReleaseKey(ui::KeyboardCode::VKEY_OEM_PLUS, ui::EF_CONTROL_DOWN);
  generator.ReleaseKey(ui::KeyboardCode::VKEY_CONTROL, 0);
  // Zoom bubble shouldn't reveal the immersive frame.
  auto* zoom_bubble_coordinator = ZoomBubbleCoordinator::From(browser());
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return !!zoom_bubble_coordinator->bubble(); }));
  EXPECT_FALSE(immersive_mode_controller->IsRevealed());
}

// Test the normal type browser's kTopViewInset is always 0.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest, TopViewInset) {
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  aura::Window* window = browser()->GetWindow()->GetNativeWindow();
  EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));

  // The kTopViewInset should be 0 when in immersive mode.
  EnterImmersiveFullscreenMode(browser());
  EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));

  // An immersive reveal shows the top of the frame.
  std::unique_ptr<ImmersiveRevealedLock> revealed_lock =
      immersive_mode_controller->GetRevealedLock(
          ImmersiveModeController::ANIMATE_REVEAL_NO);
  EXPECT_TRUE(immersive_mode_controller->IsRevealed());
  EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));

  // End the reveal and exit immersive mode.
  // The kTopViewInset should be 0 when immersive mode is exited.
  revealed_lock.reset();
  ExitImmersiveFullscreenMode(browser());
  EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));
}

// Test that for a browser window, its caption buttons are moved to the top
// container when entering immersive fullscreen and back to frame view when
// exiting, not depending on reveal state.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       CaptionButtonsMovedToTopContainer) {
  gfx::ScopedAnimationDurationScaleMode scale_mode(
      gfx::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);

  chromeos::ImmersiveFullscreenControllerTestApi::GlobalAnimationDisabler
      disabler;

  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::View* frame_view = GetFrameViewChromeOS(browser_view);
  auto* caption_buttons =
      GetFrameViewChromeOS(browser_view)->caption_button_container();
  views::View* top_container = browser_view->top_container();

  // Initially in non-immersive mode, caption buttons should be a child of the
  // frame view.
  EXPECT_EQ(frame_view, caption_buttons->parent());

  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  {
    ImmersiveModeTester tester(browser());

    // Auto Enter immersive mode.
    EnterImmersiveFullscreenMode(browser());
    tester.WaitForRevealStarted();

    // Immersive frame is revealed upon entering immersive fullscreen.
    EXPECT_TRUE(immersive_mode_controller->IsRevealed());

    // In immersive mode, caption buttons should be a child of the top
    // container.
    EXPECT_EQ(top_container, caption_buttons->parent());

    // Timeout to unreveal.
    tester.WaitForRevealEnded();
  }

  EXPECT_FALSE(immersive_mode_controller->IsRevealed());
  EXPECT_EQ(top_container, caption_buttons->parent());

  aura::Window* window = browser()->GetWindow()->GetNativeWindow();
  ui::test::EventGenerator event_generator(window->GetRootWindow());

  gfx::Point point(std::roundl(window->bounds().width() / 3), 0);
  {
    ImmersiveModeTester tester(browser());
    event_generator.MoveMouseTo(point);
    tester.WaitForRevealStarted();

    EXPECT_TRUE(immersive_mode_controller->IsRevealed());
    EXPECT_EQ(top_container, caption_buttons->parent());

    point.set_y(std::roundl(window->bounds().height() / 2));
    event_generator.MoveMouseTo(point);
    tester.WaitForRevealEnded();
  }
  EXPECT_FALSE(immersive_mode_controller->IsRevealed());

  EXPECT_EQ(top_container, caption_buttons->parent());

  // Exit immersive mode.
  ExitImmersiveFullscreenMode(browser());

  // Caption buttons should be moved back to the frame view.
  EXPECT_EQ(frame_view, caption_buttons->parent());
}

// Test that for a browser window, its caption buttons are always hidden in
// tablet mode.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       BrowserHeaderVisibilityInTabletModeTest) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::Widget* widget = browser_view->GetWidget();
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  widget->GetNativeWindow()->SetProperty(
      aura::client::kResizeBehaviorKey,
      aura::client::kResizeBehaviorCanMaximize |
          aura::client::kResizeBehaviorCanResize);

  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  // Ensure the current layout is finished before entering overview mode.
  views::test::RunScheduledLayout(browser_view);
  ASSERT_FALSE(browser_view->needs_layout());

  EnterOverviewMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  // b/362835104: Browser windows that are not web apps do not require a layout
  // after the header is made invisible. This is an optimization with no visual
  // impact. Depends on `kOverviewSessionInitOptimizations` being enabled.
  //
  ASSERT_FALSE(browser_view->GetIsWebAppType());
  EXPECT_FALSE(browser_view->needs_layout());

  ExitOverviewMode();

  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  EnterTabletMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
  EnterOverviewMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
  ExitOverviewMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
  SnapWindow(widget->GetNativeWindow(), ash::SnapPosition::kPrimary);
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
}

// Regression test for https://crbug.com/41411260.
// Tests that we don't accidentally change the color of app frame title bars.
// Update expectation if change is intentional.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest, AppFrameColor) {
  Browser* app_browser =
      CreateBrowserForApp("test_browser_app", browser()->GetProfile());

  aura::Window* window = app_browser->GetWindow()->GetNativeWindow();
  SkColor active_frame_color =
      window->GetProperty(chromeos::kFrameActiveColorKey);

  const auto* const native_theme =
      BrowserView::GetBrowserViewForBrowser(browser())->GetNativeTheme();
  const bool dark_mode = native_theme->preferred_color_scheme() ==
                         ui::NativeTheme::PreferredColorScheme::kDark;
  EXPECT_EQ(active_frame_color,
            dark_mode ? gfx::kGoogleGrey900 : SkColorSetRGB(0xFF, 0xFF, 0xFF))
      << "RGB: " << SkColorGetR(active_frame_color) << ", "
      << SkColorGetG(active_frame_color) << ", "
      << SkColorGetB(active_frame_color);
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest, AccessibleProperties) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  ui::AXNodeData data;

  frame_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(ax::mojom::Role::kTitleBar, data.role);
}

namespace {

constexpr char kCalculatorAppUrl[] = "https://calculator.apps.chrome/";

constexpr char kPreventCloseEnabledForCalculator[] = R"([
  {
    "manifest_id": "https://calculator.apps.chrome/",
    "run_on_os_login": "run_windowed",
    "prevent_close_after_run_on_os_login": true
  }
])";

constexpr char kCalculatorForceInstalled[] = R"([
  {
    "url": "https://calculator.apps.chrome/",
    "default_launch_container": "window"
  }
])";

}  // namespace

class PreventCloseBrowserFrameViewChromeOSTest : public PreventCloseTestBase {
 public:
  PreventCloseBrowserFrameViewChromeOSTest() = default;

  PreventCloseBrowserFrameViewChromeOSTest(
      const PreventCloseBrowserFrameViewChromeOSTest&) = delete;
  PreventCloseBrowserFrameViewChromeOSTest& operator=(
      const PreventCloseBrowserFrameViewChromeOSTest&) = delete;

  ~PreventCloseBrowserFrameViewChromeOSTest() override = default;

  void SetUpOnMainThread() override {
    PreventCloseTestBase::SetUpOnMainThread();
  }

  void TearDownOnMainThread() override {
    PreventCloseTestBase::TearDownOnMainThread();
  }

  views::Button* GetWindowCloseButton(BrowserWindowInterface* browser) {
    auto* const browser_view = BrowserView::GetBrowserViewForBrowser(browser);
    auto* const frame_view =
        ChromeOSBrowserUITest::GetFrameViewChromeOS(browser_view);

    chromeos::FrameCaptionButtonContainerView::TestApi test_api(
        frame_view->caption_button_container());
    return test_api.close_button();
  }
};

IN_PROC_BROWSER_TEST_F(PreventCloseBrowserFrameViewChromeOSTest,
                       CloseButtonIsDisabled) {
  InstallPWA(GURL(kCalculatorAppUrl), ash::kCalculatorAppId);
  SetPoliciesAndWaitUntilInstalled(ash::kCalculatorAppId,
                                   kPreventCloseEnabledForCalculator,
                                   kCalculatorForceInstalled);

  BrowserWindowInterface* const browser =
      LaunchPWA(ash::kCalculatorAppId, /*launch_in_window=*/true);
  ASSERT_TRUE(browser);

  {
    auto* const close_button = GetWindowCloseButton(browser);
    ASSERT_TRUE(close_button);
    EXPECT_FALSE(close_button->GetEnabled());
  }

  {
    apps::AppUpdateWaiter waiter(
        browser->GetProfile(), ash::kCalculatorAppId,
        base::BindRepeating([](const apps::AppUpdate& update) {
          return update.AllowClose().has_value() && update.AllowClose().value();
        }));
    ClearWebAppSettings();
    waiter.Await();
  }

  {
    auto* const close_button = GetWindowCloseButton(browser);
    ASSERT_TRUE(close_button);
    EXPECT_TRUE(close_button->GetEnabled());
  }
}

IN_PROC_BROWSER_TEST_F(PreventCloseBrowserFrameViewChromeOSTest,
                       CloseButtonIsEnabled) {
  InstallPWA(GURL(kCalculatorAppUrl), ash::kCalculatorAppId);

  BrowserWindowInterface* const browser =
      LaunchPWA(ash::kCalculatorAppId, /*launch_in_window=*/true);
  ASSERT_TRUE(browser);

  auto* const close_button = GetWindowCloseButton(browser);
  ASSERT_TRUE(close_button);
  EXPECT_TRUE(close_button->GetEnabled());

  ClearWebAppSettings();
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       ImmersiveModeTopViewInset) {
  Browser* app_browser =
      CreateBrowserForApp("test_browser_app", browser()->GetProfile());

  auto* const immersive_mode_controller =
      ImmersiveModeController::From(app_browser);
  aura::Window* window = app_browser->GetWindow()->GetNativeWindow();
  EXPECT_LT(0, window->GetProperty(aura::client::kTopViewInset));

  // The kTopViewInset should be 0 when in immersive mode.
  EnterImmersiveFullscreenMode(app_browser);
  EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));

  // An immersive reveal shows the top of the frame.
  std::unique_ptr<ImmersiveRevealedLock> revealed_lock =
      immersive_mode_controller->GetRevealedLock(
          ImmersiveModeController::ANIMATE_REVEAL_NO);
  EXPECT_TRUE(immersive_mode_controller->IsRevealed());
  EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));

  // End the reveal and exit immersive mode.
  // The kTopViewInset should be larger than 0 again when immersive mode is
  // exited.
  revealed_lock.reset();
  ExitImmersiveFullscreenMode(app_browser);
  EXPECT_LT(0, window->GetProperty(aura::client::kTopViewInset));

  // The kTopViewInset is the same as in overview mode.
  const int inset_normal = window->GetProperty(aura::client::kTopViewInset);
  EnterOverviewMode();
  const int inset_in_overview_mode =
      window->GetProperty(aura::client::kTopViewInset);
  EXPECT_EQ(inset_normal, inset_in_overview_mode);
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       ToggleTabletModeWhileImmersiveModeEnabled) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());

  EnterImmersiveFullscreenMode(browser());

  // Should not exit immersive mode + fullscreen when tablet mode is enabled.
  EnterTabletMode();
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());
  EXPECT_TRUE(browser_view->IsFullscreen());

  const ash::Shelf* shelf =
      ash::Shell::GetPrimaryRootWindowController()->shelf();
  EXPECT_EQ(ash::HotseatState::kHidden,
            shelf->shelf_layout_manager()->hotseat_state());

  // Swipe up/down gesture should work on immersive fullscreen.
  ui::test::EventGenerator event_generator(
      browser_view->GetNativeWindow()->GetRootWindow());
  event_generator.SetTouchRadius(10, 5);
  gfx::Rect display_bounds =
      display::Screen::Get()->GetPrimaryDisplay().bounds();
  // Swipe down gesture should reveal multitask menu.
  event_generator.PressTouch(display_bounds.top_center());
  event_generator.MoveTouchBy(0, 80);
  event_generator.ReleaseTouch();
  auto* const multitask_menu_event_handler =
      ash::TabletModeControllerTestApi()
          .tablet_mode_window_manager()
          ->tablet_mode_multitask_menu_controller();
  EXPECT_TRUE(multitask_menu_event_handler->multitask_menu());

  // Swipe up gesture should reveal shelf/hotseat.
  event_generator.PressTouch(display_bounds.bottom_center());
  event_generator.MoveTouchBy(0, -80);
  event_generator.ReleaseTouch();
  // The shelf and the hotseat should be visible.
  EXPECT_EQ(ash::HotseatState::kExtended,
            shelf->shelf_layout_manager()->hotseat_state());
  EXPECT_EQ(ash::SHELF_AUTO_HIDE_SHOWN, shelf->GetAutoHideState());

  // Should not exit immersive mode + fullscreen when tablet mode is disabled.
  ExitTabletMode();
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());
  EXPECT_TRUE(browser_view->IsFullscreen());
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewChromeOSTest,
                       ToggleImmersiveModeWhileTabletModeEnabled) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  ASSERT_FALSE(immersive_mode_controller->IsEnabled());
  ASSERT_FALSE(browser_view->IsFullscreen());

  EnterTabletMode();

  // Should be able to enter immersive mode even when the tablet mode is
  // enabled.
  EnterImmersiveFullscreenMode(browser());
}

using FloatBrowserFrameViewChromeOSTest =
    TopChromeMdParamTest<ChromeOSBrowserUITest>;

IN_PROC_BROWSER_TEST_P(FloatBrowserFrameViewChromeOSTest,
                       TabletModeMultitaskMenu) {
  gfx::ScopedAnimationDurationScaleMode test_duration_mode(
      gfx::ScopedAnimationDurationScaleMode::ZERO_DURATION);
  EnterTabletMode();

  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::Widget* widget = browser_view->GetWidget();
  aura::Window* window = widget->GetNativeWindow();
  ui::test::EventGenerator event_generator(window->GetRootWindow());

  // A normal tap on the top center of the window and in the omnibox
  // bounds will focus the omnibox.
  views::View* omnibox = browser_view->GetViewByID(VIEW_ID_OMNIBOX);
  const gfx::Rect omnibox_bounds = omnibox->GetBoundsInScreen();
  ASSERT_NO_FATAL_FAILURE(
      event_generator.GestureTapAt(omnibox_bounds.top_center()));
  EXPECT_TRUE(omnibox->HasFocus());

  // Swipe down from the top center opens the multitask menu.
  event_generator.SetTouchRadius(10, 5);
  const gfx::Point top_center(window->bounds().CenterPoint().x(), -1);
  event_generator.PressTouch(top_center);
  event_generator.MoveTouchBy(0, 100);
  event_generator.ReleaseTouch();
  EXPECT_FALSE(omnibox->HasFocus());
  auto* multitask_menu_event_handler =
      ash::TabletModeControllerTestApi()
          .tablet_mode_window_manager()
          ->tablet_mode_multitask_menu_controller();
  EXPECT_TRUE(multitask_menu_event_handler->multitask_menu());

  // Tap on the omnibox outside the menu takes focus and closes the menu.
  ASSERT_NO_FATAL_FAILURE(
      event_generator.GestureTapAt(omnibox_bounds.left_center()));
  EXPECT_TRUE(omnibox->HasFocus());
  EXPECT_FALSE(multitask_menu_event_handler->multitask_menu());
}

IN_PROC_BROWSER_TEST_P(FloatBrowserFrameViewChromeOSTest,
                       BrowserHeaderVisibilityInTabletModeTest) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  EnterTabletMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  aura::Window* window = browser_view->GetWidget()->GetNativeWindow();
  auto* immersive_controller = chromeos::ImmersiveFullscreenController::Get(
      views::Widget::GetWidgetForNativeView(window));

  // Snap the window. No immersive mode from regular browsers.
  SnapWindow(window, ash::SnapPosition::kSecondary);
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
  EXPECT_FALSE(immersive_controller->IsEnabled());

  // Float the window; the title bar becomes visible.
  chromeos::FloatControllerBase::Get()->SetFloat(
      window, chromeos::FloatStartLocation::kBottomRight);
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
  EXPECT_FALSE(immersive_controller->IsEnabled());
}

// Test that for a browser app window, its caption buttons may or may not hide
// in tablet mode.
IN_PROC_BROWSER_TEST_P(FloatBrowserFrameViewChromeOSTest,
                       BrowserAppHeaderVisibilityInTabletModeTest) {
  Browser* browser2 =
      CreateBrowserForApp("test_browser_app", browser()->GetProfile());
  BrowserView* browser_view2 = BrowserView::GetBrowserViewForBrowser(browser2);
  views::Widget* widget2 = browser_view2->GetWidget();
  BrowserFrameViewChromeOS* frame_view2 = GetFrameViewChromeOS(browser_view2);
  widget2->GetNativeWindow()->SetProperty(
      aura::client::kResizeBehaviorKey,
      aura::client::kResizeBehaviorCanMaximize |
          aura::client::kResizeBehaviorCanResize);

  EnterOverviewMode();
  EXPECT_FALSE(frame_view2->caption_button_container()->GetVisible());
  ExitOverviewMode();
  EXPECT_TRUE(frame_view2->caption_button_container()->GetVisible());

  EnterTabletMode();
  EnterOverviewMode();
  EXPECT_FALSE(frame_view2->caption_button_container()->GetVisible());
  ExitOverviewMode();
  EXPECT_TRUE(frame_view2->caption_button_container()->GetVisible());
  auto* immersive_controller = chromeos::ImmersiveFullscreenController::Get(
      views::Widget::GetWidgetForNativeView(widget2->GetNativeWindow()));
  EXPECT_TRUE(widget2->IsMaximized());
  // App type will use immersive.
  EXPECT_TRUE(immersive_controller->IsEnabled());

  // Snap a window. Immersive mode is enabled so its title bar is not visible.
  SnapWindow(widget2->GetNativeWindow(), ash::SnapPosition::kSecondary);
  EXPECT_TRUE(frame_view2->caption_button_container()->GetVisible());
  EXPECT_TRUE(immersive_controller->IsEnabled());

  // Float a window. Immersive mode is disabled so its title bar is visible.
  ui::test::EventGenerator event_generator(
      widget2->GetNativeWindow()->GetRootWindow());
  event_generator.PressAndReleaseKeyAndModifierKeys(
      ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(frame_view2->caption_button_container()->GetVisible());
  EXPECT_FALSE(immersive_controller->IsEnabled());
}

// Tests that, with the float flag enabled, the accelerator toggles the
// multitask menu on a browser window.
IN_PROC_BROWSER_TEST_P(FloatBrowserFrameViewChromeOSTest, ToggleMultitaskMenu) {
  EXPECT_TRUE(chrome::IsCommandEnabled(browser(), IDC_TOGGLE_MULTITASK_MENU));

  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  auto* size_button = static_cast<chromeos::FrameSizeButton*>(
      frame_view->caption_button_container()->size_button());

  // Pressing accelerator once should show the multitask menu.
  ui::test::EventGenerator event_generator(
      browser_view->GetWidget()->GetNativeWindow()->GetRootWindow());
  event_generator.PressAndReleaseKeyAndModifierKeys(ui::VKEY_Z,
                                                    ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(size_button->IsMultitaskMenuShown());

  // With platform bubble, key event is routed to the platform bubble at ozone
  // level, so dispatch it to the multitask_menu_widget directly.
  if (views::test::IsOzoneBubblesUsingPlatformWidgets()) {
    ui::test::EventGenerator multitask_view_event_generator(
        size_button->multitask_menu_widget_for_testing()
            ->GetNativeWindow()
            ->GetRootWindow());
    // Pressing accelerator a second time should close the menu.
    multitask_view_event_generator.PressAndReleaseKeyAndModifierKeys(
        ui::VKEY_Z, ui::EF_COMMAND_DOWN);
  } else {
    // Pressing accelerator a second time should close the menu.
    event_generator.PressAndReleaseKeyAndModifierKeys(ui::VKEY_Z,
                                                      ui::EF_COMMAND_DOWN);
  }

  EXPECT_FALSE(size_button->IsMultitaskMenuShown());
}

IN_PROC_BROWSER_TEST_P(FloatBrowserFrameViewChromeOSTest,
                       TabletModeRestoreFromFloat) {
  EnterTabletMode();

  const BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(browser());
  auto* native_window = browser_view->GetNativeWindow();
  ui::test::EventGenerator event_generator(native_window->GetRootWindow());
  // Switch to float state.
  event_generator.PressAndReleaseKeyAndModifierKeys(
      ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(ash::WindowState::Get(native_window)->IsFloated());

  // Restore from float state.
  event_generator.PressAndReleaseKeyAndModifierKeys(
      ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  EXPECT_TRUE(ash::WindowState::Get(native_window)->IsMaximized());

  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());
}

using HomeLauncherBrowserFrameViewChromeOSTest = BrowserFrameViewChromeOSTest;

IN_PROC_BROWSER_TEST_P(HomeLauncherBrowserFrameViewChromeOSTest,
                       TabletModeBrowserCaptionButtonVisibility) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);

  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  EnterTabletMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  EnterOverviewMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
  ExitOverviewMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  ExitTabletMode();

  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
}

// TODO(crbug.com/40640473): When the test flake has been addressed, improve
// performance by consolidating this unit test with
// |TabletModeBrowserCaptionButtonVisibility|. Do not forget to remove the
// corresponding |FRIEND_TEST_ALL_PREFIXES| usage from
// |BrowserFrameViewChromeOS|.
IN_PROC_BROWSER_TEST_P(HomeLauncherBrowserFrameViewChromeOSTest,
                       CaptionButtonVisibilityForBrowserLaunchedInTabletMode) {
  EnterTabletMode();
  auto* new_browser = CreateBrowser(browser()->GetProfile());
  auto* frame_view =
      GetFrameViewChromeOS(BrowserView::GetBrowserViewForBrowser(new_browser));
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
}

IN_PROC_BROWSER_TEST_P(HomeLauncherBrowserFrameViewChromeOSTest,
                       TabletModeAppCaptionButtonVisibility) {
  Browser* app_browser =
      CreateBrowserForApp("test_browser_app", browser()->GetProfile());
  BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(app_browser);
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(app_browser);
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());

  // Tablet mode doesn't affect app's caption button's visibility.
  EnterTabletMode();
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
  EXPECT_FALSE(browser_view->IsFullscreen());
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());

  // However, overview mode does.
  EnterOverviewMode();
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
  ExitOverviewMode();
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  ExitTabletMode();
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
  EXPECT_FALSE(browser_view->IsFullscreen());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());
}

using LockedFullscreenBrowserFrameViewChromeOSTest =
    TopChromeMdParamTest<ChromeOSBrowserUITest>;

IN_PROC_BROWSER_TEST_P(LockedFullscreenBrowserFrameViewChromeOSTest,
                       ToggleTabletModeWhenNotLockedForOnTask) {
  ash::boca::OnTaskLockedController::From(browser())->set_locked_for_on_task(
      false);
  BrowserView* const browser_view =
      BrowserView::GetBrowserViewForBrowser(browser());
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());

  // Set locked fullscreen state.
  PinWindow(browser_view->GetWidget()->GetNativeWindow(), /*trusted=*/true);

  // We're fullscreen, immersive is disabled in locked fullscreen, and while
  // we're at it, also make sure that the shelf is hidden.
  EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());
  EXPECT_FALSE(IsShelfVisible());

  auto* const widget = browser_view->GetWidget();
  auto* const immersive_controller =
      chromeos::ImmersiveFullscreenController::Get(
          views::Widget::GetWidgetForNativeView(widget->GetNativeWindow()));
  EXPECT_FALSE(immersive_controller->IsEnabled());

  // Caption buttons should be hidden because immersive mode is disabled.
  BrowserFrameViewChromeOS* const frame_view =
      GetFrameViewChromeOS(browser_view);
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  EnterTabletMode();
  EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());
  EXPECT_FALSE(IsShelfVisible());
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());

  ExitTabletMode();
  EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());
  EXPECT_FALSE(IsShelfVisible());
  EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
}

IN_PROC_BROWSER_TEST_P(LockedFullscreenBrowserFrameViewChromeOSTest,
                       ToggleTabletModeWhenLockedForOnTask) {
  ash::boca::OnTaskLockedController::From(browser())->set_locked_for_on_task(
      true);
  BrowserView* const browser_view =
      BrowserView::GetBrowserViewForBrowser(browser());
  auto* const immersive_mode_controller =
      ImmersiveModeController::From(browser());
  EXPECT_FALSE(immersive_mode_controller->IsEnabled());

  // Set locked fullscreen state.
  PinWindow(browser_view->GetWidget()->GetNativeWindow(), /*trusted=*/true);

  // Verify immersive mode is enabled in locked fullscreen mode and when locked
  // for OnTask. Also verify that the shelf is hidden.
  EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());
  EXPECT_FALSE(IsShelfVisible());

  auto* const widget = browser_view->GetWidget();
  auto* const immersive_controller =
      chromeos::ImmersiveFullscreenController::Get(
          views::Widget::GetWidgetForNativeView(widget->GetNativeWindow()));
  EXPECT_TRUE(immersive_controller->IsEnabled());

  // Verify caption buttons are visible before entering tablet mode.
  BrowserFrameViewChromeOS* const frame_view =
      GetFrameViewChromeOS(browser_view);
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  EnterTabletMode();
  EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());
  EXPECT_FALSE(IsShelfVisible());
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());

  ExitTabletMode();
  EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
  EXPECT_TRUE(immersive_mode_controller->IsEnabled());
  EXPECT_FALSE(IsShelfVisible());
  EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
}

class BrowserFrameViewAshAvatarTest : public BrowserFrameViewChromeOSTest {
 public:
  BrowserFrameViewAshAvatarTest() = default;

  static constexpr inline auto kPrimaryAccountId =
      AccountId::Literal::FromUserEmailGaiaId("primary@test",
                                              GaiaId::Literal("12345"));

  static constexpr inline auto kSecondaryAccountId =
      AccountId::Literal::FromUserEmailGaiaId("secondary@test",
                                              GaiaId::Literal("67890"));

  void SetUp() override {
    set_exit_when_last_browser_closes(false);
    signin::AccountManagedStatusFinder::SetNonEnterpriseDomainForTesting(
        "test");
    BrowserFrameViewChromeOSTest::SetUp();
  }

  void TearDown() override {
    BrowserFrameViewChromeOSTest::TearDown();
    signin::AccountManagedStatusFinder::SetNonEnterpriseDomainForTesting(
        nullptr);
  }

  void LogIn(const AccountId& account_id) {
    // If there's already a session, i.e. if this is multi user sign in,
    // first, launch user selection flow.
    if (auto* primary_session =
            session_manager::SessionManager::Get()->GetPrimarySession()) {
      // Mark the acknowledge for the multi-user sign-in.
      user_manager::User* primary_user =
          user_manager::UserManager::Get()->FindUserAndModify(
              primary_session->account_id());
      primary_user->GetProfilePrefs()->SetBoolean(
          user_manager::prefs::kMultiProfileNeverShowIntro, true);
      SessionControllerClientImpl::Get()->ShowMultiProfileLogin();
    }
    login_manager_mixin_.LoginWithDefaultContext(
        ash::LoginManagerMixin::TestUserInfo(account_id));
  }

 private:

  ash::DeviceStateMixin device_state_{
      &mixin_host_,
      ash::DeviceStateMixin::State::OOBE_COMPLETED_PERMANENTLY_UNOWNED};

  ash::LoginManagerMixin login_manager_mixin_{
      &mixin_host_,
      {ash::LoginManagerMixin::TestUserInfo(kPrimaryAccountId),
       ash::LoginManagerMixin::TestUserInfo(kSecondaryAccountId)}};
};

// Tests that Avatar icon should show on the top left corner of the teleported
// browser window on ChromeOS.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewAshAvatarTest,
                       AvatarDisplayOnTeleportedWindow) {
  LogIn(kPrimaryAccountId);
  Profile* primary_user_profile = Profile::FromBrowserContext(
      ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
          kPrimaryAccountId));

  ash::NewWindowDelegate::GetInstance()->NewWindow(
      /*incognito=*/false, /*should_trigger_session_restore=*/false);
  BrowserWindowInterface* browser =
      ProfileBrowserCollection::GetForProfile(primary_user_profile)
          ->GetLastActiveBrowser();
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  BrowserFrameViewChromeOSTestApi test_api(frame_view);
  aura::Window* window = browser->GetWindow()->GetNativeWindow();

  EXPECT_FALSE(BrowserFrameViewChromeOS::ShouldShowAvatarForTesting(window));
  EXPECT_FALSE(test_api.GetProfileIndicatorIcon());

  // Log in with the secondary user.
  LogIn(kSecondaryAccountId);

  // Move back to the primary user's desktop.
  SessionControllerClientImpl::Get()->SwitchActiveUser(kPrimaryAccountId);
  ASSERT_EQ(
      session_manager::SessionManager::Get()->GetActiveSession()->account_id(),
      kPrimaryAccountId);

  auto* window_manager = ash::Shell::Get()->multi_user_window_manager();

  // Teleport the window to secondary user's desktop.
  browser_view->Activate();
  window_manager->ShowWindowForUser(window, kSecondaryAccountId);
  ASSERT_EQ(
      session_manager::SessionManager::Get()->GetActiveSession()->account_id(),
      kSecondaryAccountId);

  EXPECT_TRUE(BrowserFrameViewChromeOS::ShouldShowAvatarForTesting(window));
  auto* icon = test_api.GetProfileIndicatorIcon();
  ASSERT_TRUE(icon);

  // Verify that the avatar icon is vertically centered within the tabstrip
  // area.
  const int expected_frame_height =
      frame_view->GetTopInset(false) +
      browser_view->GetFrameElementInfo().tabstrip_preferred_height;
  const int expected_icon_y = (expected_frame_height - icon->height()) / 2;
  EXPECT_EQ(expected_icon_y, icon->bounds().y());

  // Teleport the window back to owner desktop.
  browser_view->Activate();
  window_manager->ShowWindowForUser(window, kPrimaryAccountId);
  EXPECT_FALSE(BrowserFrameViewChromeOS::ShouldShowAvatarForTesting(window));
  EXPECT_FALSE(test_api.GetProfileIndicatorIcon());
}

// Tests that the profile indicator icon is positioned within the browser
// bounds on both horizontal and vertical tab strip.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewAshAvatarTest,
                       ProfileIndicatorPositionWithinBounds) {
  LogIn(kPrimaryAccountId);
  Profile* primary_user_profile = Profile::FromBrowserContext(
      ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
          kPrimaryAccountId));

  ash::NewWindowDelegate::GetInstance()->NewWindow(
      /*incognito=*/false, /*should_trigger_session_restore=*/false);
  BrowserWindowInterface* browser =
      ProfileBrowserCollection::GetForProfile(primary_user_profile)
          ->GetLastActiveBrowser();
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  BrowserFrameViewChromeOSTestApi test_api(frame_view);
  aura::Window* window = browser->GetWindow()->GetNativeWindow();

  // Log in with the secondary user.
  LogIn(kSecondaryAccountId);

  // Move back to the primary user's desktop.
  SessionControllerClientImpl::Get()->SwitchActiveUser(kPrimaryAccountId);
  ASSERT_EQ(
      session_manager::SessionManager::Get()->GetActiveSession()->account_id(),
      kPrimaryAccountId);

  auto* window_manager = ash::Shell::Get()->multi_user_window_manager();

  // Teleport the window to secondary user's desktop.
  browser_view->Activate();
  window_manager->ShowWindowForUser(window, kSecondaryAccountId);
  ASSERT_EQ(
      session_manager::SessionManager::Get()->GetActiveSession()->account_id(),
      kSecondaryAccountId);

  EXPECT_TRUE(BrowserFrameViewChromeOS::ShouldShowAvatarForTesting(window));
  auto* icon = test_api.GetProfileIndicatorIcon();
  ASSERT_TRUE(icon);
  EXPECT_TRUE(icon->GetVisible());

  // Horizontal tab strip mode (default). Verify that the profile indicator icon
  // is positioned within the browser frame bounds.
  EXPECT_FALSE(browser_view->ShouldDrawVerticalTabStrip());
  EXPECT_GE(icon->bounds().y(), 0);
  EXPECT_LE(icon->bounds().bottom(), frame_view->height());
  EXPECT_TRUE(frame_view->GetLocalBounds().Contains(icon->bounds()));

  // Switch to vertical tab strip mode.
  auto* controller = tabs::VerticalTabStripStateController::From(browser);
  ASSERT_TRUE(controller);
  controller->SetVerticalTabsEnabled(true);
  browser_view->GetWidget()->LayoutRootViewIfNecessary();

  // Verify that in vertical tab strip mode, the profile indicator icon is also
  // positioned within the browser frame bounds.
  EXPECT_TRUE(browser_view->ShouldDrawVerticalTabStrip());
  EXPECT_TRUE(icon->GetVisible());
  EXPECT_GE(icon->bounds().y(), 0);
  EXPECT_LE(icon->bounds().bottom(), frame_view->height());
  EXPECT_TRUE(frame_view->GetLocalBounds().Contains(icon->bounds()));
}

// Regression test for b/527095091.
IN_PROC_BROWSER_TEST_P(BrowserFrameViewAshAvatarTest,
                       ProfileIconPositionUpdatesAppTitle) {
  LogIn(kPrimaryAccountId);
  Profile* primary_user_profile = Profile::FromBrowserContext(
      ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
          kPrimaryAccountId));

  const auto app_id = web_app::test::InstallDummyWebApp(
      primary_user_profile, "test_browser_app", GURL("https://test.org"));
  BrowserWindowInterface* app_browser =
      web_app::LaunchWebAppBrowser(primary_user_profile, app_id);
  BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(app_browser);
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  BrowserFrameViewChromeOSTestApi test_api(frame_view);
  aura::Window* window = app_browser->GetWindow()->GetNativeWindow();

  // Force initial layout.
  browser_view->GetWidget()->LayoutRootViewIfNecessary();

  // The app title and toolbar are drawn by the WebAppFrameToolbarView.
  ASSERT_TRUE(browser_view->web_app_frame_toolbar_for_testing());
  const int title_x_without_icon =
      browser_view->web_app_frame_toolbar_for_testing()->x();

  // Log in with the secondary user.
  LogIn(kSecondaryAccountId);

  // Move back to the primary user's desktop.
  SessionControllerClientImpl::Get()->SwitchActiveUser(kPrimaryAccountId);

  // Teleport the window to secondary user's desktop. This adds the avatar icon.
  auto* window_manager = ash::Shell::Get()->multi_user_window_manager();
  browser_view->Activate();
  window_manager->ShowWindowForUser(window, kSecondaryAccountId);
  auto* icon = test_api.GetProfileIndicatorIcon();
  ASSERT_TRUE(icon);

  // Force layout to apply the teleportation changes.
  browser_view->GetWidget()->LayoutRootViewIfNecessary();
  const int title_x_with_icon =
      browser_view->web_app_frame_toolbar_for_testing()->x();

  // The title should be pushed to the right by the icon.
  EXPECT_GT(title_x_with_icon, title_x_without_icon);

  // Teleport back to remove the icon.
  window_manager->ShowWindowForUser(window, kPrimaryAccountId);
  browser_view->Activate();
  EXPECT_FALSE(test_api.GetProfileIndicatorIcon());

  // Force layout again.
  browser_view->GetWidget()->LayoutRootViewIfNecessary();
  EXPECT_EQ(title_x_without_icon,
            browser_view->web_app_frame_toolbar_for_testing()->x());
}

IN_PROC_BROWSER_TEST_P(BrowserFrameViewAshAvatarTest,
                       ProfileIconPositionUpdatesTitleForNonTabbedWindow) {
  LogIn(kPrimaryAccountId);
  Profile* primary_user_profile = Profile::FromBrowserContext(
      ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
          kPrimaryAccountId));

  // We use a DevTools window here as a representative example of a non-tabbed
  // browser window that renders a native title in its frame.
  BrowserWindowInterface* non_tabbed_browser = CreateBrowserWindow(
      BrowserWindowCreateParams::CreateForDevTools(primary_user_profile));
  non_tabbed_browser->GetWindow()->Show();

  BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(non_tabbed_browser);
  BrowserFrameViewChromeOS* frame_view = GetFrameViewChromeOS(browser_view);
  BrowserFrameViewChromeOSTestApi test_api(frame_view);
  aura::Window* window = non_tabbed_browser->GetWindow()->GetNativeWindow();
  auto* frame_header = test_api.GetFrameHeader();

  // Force initial layout.
  browser_view->GetWidget()->LayoutRootViewIfNecessary();

  // The title is drawn natively by FrameHeader for non-tabbed windows.
  const int title_x_without_icon = frame_header->GetTitleBoundsForTesting().x();

  // Log in with the secondary user.
  LogIn(kSecondaryAccountId);

  // Move back to the primary user's desktop.
  SessionControllerClientImpl::Get()->SwitchActiveUser(kPrimaryAccountId);

  // Teleport the window to secondary user's desktop. This adds the avatar icon.
  auto* window_manager = ash::Shell::Get()->multi_user_window_manager();
  browser_view->Activate();
  window_manager->ShowWindowForUser(window, kSecondaryAccountId);
  auto* icon = test_api.GetProfileIndicatorIcon();
  ASSERT_TRUE(icon);

  // Force layout to apply the teleportation changes.
  browser_view->GetWidget()->LayoutRootViewIfNecessary();
  const int title_x_with_icon = frame_header->GetTitleBoundsForTesting().x();

  // The title should be pushed to the right by the icon.
  EXPECT_GT(title_x_with_icon, title_x_without_icon);

  // Teleport back to remove the icon.
  window_manager->ShowWindowForUser(window, kPrimaryAccountId);
  browser_view->Activate();
  EXPECT_FALSE(test_api.GetProfileIndicatorIcon());

  // Force layout again.
  browser_view->GetWidget()->LayoutRootViewIfNecessary();
  EXPECT_EQ(title_x_without_icon, frame_header->GetTitleBoundsForTesting().x());
}

using BrowserFrameViewAshTest = BrowserFrameViewChromeOSTest;

IN_PROC_BROWSER_TEST_P(BrowserFrameViewAshTest,
                       SettingsSystemWebAppHasMinimumWindowSize) {
  // Install the Settings System Web App.
  ash::SystemWebAppManager::GetForTest(browser()->GetProfile())
      ->InstallSystemAppsForTesting();

  // Open a settings window.
  ui_test_utils::BrowserCreatedObserver browser_created_observer;
  auto* settings_manager = chrome::SettingsWindowManager::GetInstance();
  settings_manager->ShowOSSettings(browser()->GetProfile());
  browser_created_observer.Wait();

  BrowserWindowInterface* settings_browser =
      settings_manager->FindBrowserForProfile(browser()->GetProfile());

  // Try to set the bounds to a tiny value.
  settings_browser->GetWindow()->SetBounds(gfx::Rect(1, 1));

  // The window has a reasonable size.
  gfx::Rect actual_bounds = settings_browser->GetWindow()->GetBounds();
  EXPECT_LE(300, actual_bounds.width());
  EXPECT_LE(100, actual_bounds.height());
}

// Enumeration of test modes for
// `BrowserFrameViewAshThemeChangeTest`s.
enum class ThemeChangeTestMode {
  kSWA,
  kNonSWA,
};

// Base class for `ThemeChange` tests, parameterized by test mode and:
// * whether manifest background color is preferred,
// * whether theme changes should be animated.
class BrowserFrameViewAshThemeChangeTest
    : public InProcessBrowserTest,
      public testing::WithParamInterface<ThemeChangeTestMode> {
 public:
  BrowserFrameViewAshThemeChangeTest() {
    switch (GetThemeChangeTestMode()) {
      case ThemeChangeTestMode::kSWA: {
        system_web_app_installation_ =
            ash::TestSystemWebAppInstallation::SetUpAppWithColors(
                /*theme_color=*/SK_ColorWHITE,
                /*dark_mode_theme_color=*/SK_ColorBLACK,
                /*background_color=*/SK_ColorWHITE,
                /*dark_mode_background_color=*/SK_ColorBLACK);
        auto* delegate = static_cast<ash::UnittestingSystemAppDelegate*>(
            system_web_app_installation_->GetDelegate());
        // When system colored SWAs were introduced for Jelly,
        // `UseSystemThemeColor()` overrode other styling information in the
        // manifest. This test now verifies behavior for SWAs that are opted out
        // of the system styling (by setting it to false).
        delegate->SetUseSystemThemeColor(false);
        break;
      }
      case ThemeChangeTestMode::kNonSWA:
        break;
    }
  }

  // Returns test mode given test parameterization.
  ThemeChangeTestMode GetThemeChangeTestMode() const { return GetParam(); }

  // Toggles the color mode, triggering propagation of theme change events.
  void ToggleColorMode() {
    os_settings_provider_.SetPreferredColorScheme(
        (os_settings_provider_.PreferredColorScheme() ==
         ui::NativeTheme::PreferredColorScheme::kDark)
            ? ui::NativeTheme::PreferredColorScheme::kLight
            : ui::NativeTheme::PreferredColorScheme::kDark);
  }

  // Installs the web app under test, blocking until installation is complete,
  // and returning the `webapps::AppId` for the installed web app.
  webapps::AppId WaitForAppInstall() {
    switch (GetThemeChangeTestMode()) {
      case ThemeChangeTestMode::kSWA:
        system_web_app_installation_->WaitForAppInstall();
        return system_web_app_installation_->GetAppId();
      case ThemeChangeTestMode::kNonSWA: {
        if (!test_server_) {
          test_server_ = std::make_unique<net::EmbeddedTestServer>(
              net::EmbeddedTestServer::TYPE_HTTPS);
          test_server_->AddDefaultHandlers(GetChromeTestDataDir());
          CHECK(test_server_->Start());
        }
        const GURL app_url =
            test_server_->GetURL("app.com", "/ssl/google.html");
        auto web_app_info =
            web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(app_url);
        web_app_info->scope = app_url.GetWithoutFilename();
        web_app_info->theme_color = SK_ColorWHITE;
        web_app_info->dark_mode_theme_color = SK_ColorBLACK;
        web_app_info->background_color = SK_ColorWHITE;
        web_app_info->dark_mode_background_color = SK_ColorBLACK;
        return web_app::test::InstallWebApp(profile(), std::move(web_app_info));
      }
    }
  }

  // Returns the `Profile` associated with the web app under test.
  Profile* profile() { return browser()->GetProfile(); }

 private:
  ui::MockOsSettingsProvider os_settings_provider_;
  std::unique_ptr<ash::TestSystemWebAppInstallation>
      system_web_app_installation_;
  std::unique_ptr<net::EmbeddedTestServer> test_server_;
};

INSTANTIATE_TEST_SUITE_P(
    Mode,
    BrowserFrameViewAshThemeChangeTest,
    testing::Values(ThemeChangeTestMode::kSWA, ThemeChangeTestMode::kNonSWA),
    [](const testing::TestParamInfo<ThemeChangeTestMode>& info) {
      ThemeChangeTestMode test_mode = info.param;
      switch (test_mode) {
        case ThemeChangeTestMode::kSWA:
          return "kSWA";
        case ThemeChangeTestMode::kNonSWA:
          return "kNonSWA";
      }
    });

IN_PROC_BROWSER_TEST_P(BrowserFrameViewAshThemeChangeTest, ThemeChange) {
  const webapps::AppId app_id = WaitForAppInstall();

  // Trigger the launch but do not wait for the web contents to load.
  content::WebContents* web_contents =
      apps::AppServiceProxyFactory::GetForProfile(profile())
          ->BrowserAppLauncher()
          ->LaunchAppWithParamsForTesting(apps::AppLaunchParams(
              app_id, apps::LaunchContainer::kLaunchContainerWindow,
              WindowOpenDisposition::CURRENT_TAB,
              apps::LaunchSource::kFromTest));
  ASSERT_TRUE(web_contents);
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents);
  auto* contents_web_view =
      BrowserView::GetBrowserViewForBrowser(browser)->contents_web_view();

  // Verify background color is immediately resolved from the app controller
  // despite the fact that the web contents background color hasn't loaded
  // yet.
  EXPECT_EQ(contents_web_view->layer()
                ->AsSolidColor()
                ->background_color()
                .toSkColor(),
            web_app::AppBrowserController::From(browser)
                ->GetBackgroundColor()
                .value());
  EXPECT_FALSE(web_contents->GetBackgroundColor().has_value());

  // Wait for the web contents background color to load and verify that the
  // background color still matches that resolved from the app controller.
  {
    content::BackgroundColorChangeWaiter waiter(web_contents);
    waiter.Wait();
    EXPECT_EQ(contents_web_view->layer()
                  ->AsSolidColor()
                  ->background_color()
                  .toSkColor(),
              web_app::AppBrowserController::From(browser)
                  ->GetBackgroundColor()
                  .value());
    EXPECT_EQ(contents_web_view->layer()
                  ->AsSolidColor()
                  ->background_color()
                  .toSkColor(),
              web_contents->GetBackgroundColor().value());
  }

  content::AwaitDocumentOnLoadCompleted(web_contents);

  // Toggle color mode and verify background color is immediately resolved
  // from the app controller. If a system web app is loaded which prefers
  // manifest colors, there will be a temporary mismatch between the contents
  // background color and the web contents background color due to the fact
  // that the web contents background color update is async.
  ToggleColorMode();
  EXPECT_EQ(contents_web_view->layer()
                ->AsSolidColor()
                ->background_color()
                .toSkColor(),
            web_app::AppBrowserController::From(browser)
                ->GetBackgroundColor()
                .value());
  EXPECT_EQ(contents_web_view->layer()
                ->AsSolidColor()
                ->background_color()
                .toSkColor(),
            web_contents->GetBackgroundColor().value());

  // Verify that the layer associated with the `contents_web_view` native view
  // is not hidden.
  auto* layer = contents_web_view->holder()->native_view()->layer();
  EXPECT_EQ(layer->GetTargetOpacity(), 1.f);

  // Wait for the web contents background color to update and verify that the
  // background color still matches that resolved from the app controller.
  {
    content::BackgroundColorChangeWaiter waiter(web_contents);
    waiter.Wait();
    EXPECT_EQ(contents_web_view->layer()
                  ->AsSolidColor()
                  ->background_color()
                  .toSkColor(),
              web_app::AppBrowserController::From(browser)
                  ->GetBackgroundColor()
                  .value());
    EXPECT_EQ(contents_web_view->layer()
                  ->AsSolidColor()
                  ->background_color()
                  .toSkColor(),
              web_contents->GetBackgroundColor().value());
  }
}

#define INSTANTIATE_TEST_SUITE(name) \
  INSTANTIATE_TEST_SUITE_P(All, name, ::testing::Values(false, true))

INSTANTIATE_TEST_SUITE(BrowserFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(FloatBrowserFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(HomeLauncherBrowserFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(LockedFullscreenBrowserFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(WebAppFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(BrowserFrameViewAshTest);
INSTANTIATE_TEST_SUITE(BrowserFrameViewAshAvatarTest);
