// 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_non_client_frame_view_chromeos.h"
#include <string>
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/run_until.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/app_service/app_launch_params.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/prefs/session_startup_pref.h"
#include "chrome/browser/sessions/session_restore_test_helper.h"
#include "chrome/browser/sessions/session_service_test_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chromeos/test_util.h"
#include "chrome/browser/ui/passwords/passwords_client_ui_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model.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.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/webui_tab_strip_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/zoom_bubble_view.h"
#include "chrome/browser/ui/views/page_action/page_action_icon_view.h"
#include "chrome/browser/ui/views/page_info/page_info_bubble_view_base.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/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_id_constants.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/crosapi/mojom/test_controller.mojom-test-utils.h"
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "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/multitask_menu/float_controller_base.h"
#include "components/account_id/account_id.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_update.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/browser_test.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/scoped_animation_duration_scale_mode.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/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/focus/focus_manager.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"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/test/shell_test_api.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 "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/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
#include "chrome/browser/ui/ash/multi_user/test_multi_user_window_manager.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "content/public/test/background_color_change_waiter.h"
#else // BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/lacros/browser_test_util.h"
#include "chrome/browser/ui/lacros/window_properties.h"
#include "chrome/browser/web_applications/app_service/test/loopback_crosapi_app_service_proxy.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_lacros.h"
#endif
namespace {
bool WaitForFocus(bool expected, views::View* view) {
return base::test::RunUntil([&]() { return view->HasFocus() == expected; });
}
bool WaitForVisible(bool expected, views::View* view) {
return base::test::RunUntil([&]() { return view->GetVisible() == expected; });
}
bool WaitForPaintAsActive(bool expected, views::FrameCaptionButton* button) {
return base::test::RunUntil(
[&]() { return button->GetPaintAsActive() == expected; });
}
} // namespace
using BrowserNonClientFrameViewChromeOSTest =
TopChromeMdParamTest<ChromeOSBrowserUITest>;
using BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip =
WebUiTabStripOverrideTest<false, BrowserNonClientFrameViewChromeOSTest>;
using BrowserNonClientFrameViewChromeOSTestWithWebUiTabStrip =
WebUiTabStripOverrideTest<true, BrowserNonClientFrameViewChromeOSTest>;
class BrowserNonClientFrameViewChromeOSTestApi {
public:
explicit BrowserNonClientFrameViewChromeOSTestApi(
BrowserNonClientFrameViewChromeOS* frame_view)
: frame_view_(frame_view) {}
BrowserNonClientFrameViewChromeOSTestApi(
const BrowserNonClientFrameViewChromeOSTestApi&) = delete;
BrowserNonClientFrameViewChromeOSTestApi& operator=(
const BrowserNonClientFrameViewChromeOSTestApi&) = delete;
~BrowserNonClientFrameViewChromeOSTestApi() = 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<BrowserNonClientFrameViewChromeOS> frame_view_;
};
// This test does not make sense for the webUI tabstrip, since the window layout
// is different in that case.
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip,
NonClientHitTest) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
views::Widget* widget = browser_view->GetWidget();
BrowserNonClientFrameViewChromeOS* 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();
ASSERT_TRUE(base::test::RunUntil([&]() {
return window->GetProperty(chromeos::kWindowStateTypeKey) ==
chromeos::WindowStateType::kMaximized;
}));
// TODO(crbug.com/40276379): Remove waiting for bounds change when the bug
// is fixed.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return frame_view->bounds() != old_bounds; }));
}
EXPECT_EQ(HTCLIENT, frame_view->NonClientHitTest(top_edge));
}
// 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(BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip,
ContentWindowAcceptsEdgeInputsWhenMaximized) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
content::WebContents* web_contents = browser_view->GetActiveWebContents();
views::Widget* widget = browser_view->GetWidget();
const BrowserNonClientFrameViewChromeOS* 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();
ASSERT_TRUE(base::test::RunUntil([&]() {
return window->GetProperty(chromeos::kWindowStateTypeKey) ==
chromeos::WindowStateType::kMaximized;
}));
// TODO(crbug.com/40276379): Remove waiting for bounds change when the bug
// is fixed.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return frame_view->bounds() != old_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 BrowserNonClientFrameViewChromeOSTouchTest =
TopChromeTouchTest<ChromeOSBrowserUITest>;
using BrowserNonClientFrameViewChromeOSTouchTestWithWebUiTabStrip =
WebUiTabStripOverrideTest<true, BrowserNonClientFrameViewChromeOSTouchTest>;
IN_PROC_BROWSER_TEST_F(
BrowserNonClientFrameViewChromeOSTouchTestWithWebUiTabStrip,
TabletSplitViewNonClientHitTest) {
if (!IsSnapWindowSupported()) {
GTEST_SKIP() << "Ash is too old.";
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
views::Widget* widget = browser_view->GetWidget();
aura::Window* window = widget->GetNativeWindow();
const int expect_y =
frame_view->GetBorder() ? frame_view->GetBorder()->GetInsets().top() : 0;
EXPECT_EQ(expect_y, frame_view->GetBoundsForClientView().y());
EnterTabletMode();
SnapWindow(window, crosapi::mojom::SnapPosition::kPrimary);
// Touch on the top of the window is interpreted as client hit.
gfx::Point top_point(widget->GetWindowBoundsInScreen().width() / 2, 0);
EXPECT_EQ(HTCLIENT, frame_view->NonClientHitTest(top_point));
}
IN_PROC_BROWSER_TEST_F(
BrowserNonClientFrameViewChromeOSTouchTestWithWebUiTabStrip,
TabletSplitViewSwipeDownFromEdgeOpensWebUiTabStrip) {
if (!IsSnapWindowSupported()) {
GTEST_SKIP() << "Ash is too old.";
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
const int expect_y =
frame_view->GetBorder() ? frame_view->GetBorder()->GetInsets().top() : 0;
EXPECT_EQ(expect_y, frame_view->GetBoundsForClientView().y());
views::Widget* widget = browser_view->GetWidget();
EnterTabletMode();
SnapWindow(widget->GetNativeWindow(), crosapi::mojom::SnapPosition::kPrimary);
// A point at the top of the window, but not in the center horizontally, as a
// swipe down from the top center will show the chromeos tablet mode multitask
// menu.
gfx::Point edge_point(100, 0);
ASSERT_FALSE(browser_view->webui_tab_strip()->GetVisible());
aura::Window* window = widget->GetNativeWindow();
ui::test::EventGenerator event_generator(window->GetRootWindow());
event_generator.SetTouchRadius(10, 5);
event_generator.PressTouch(edge_point);
event_generator.MoveTouchBy(0, 100);
event_generator.ReleaseTouch();
ASSERT_TRUE(WaitForVisible(true, browser_view->webui_tab_strip()));
}
// Test that the frame view does not do any painting in non-immersive
// fullscreen.
// This test does not make sense for the webUI tabstrip, since the frame is not
// painted in that case.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO(crbug.com/40276379): Reenable when bug is fixed.
#define MAYBE_NonImmersiveFullscreen DISABLED_NonImmersiveFullscreen
#else
#define MAYBE_NonImmersiveFullscreen NonImmersiveFullscreen
#endif
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip,
MAYBE_NonImmersiveFullscreen) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
content::WebContents* web_contents = browser_view->GetActiveWebContents();
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
BrowserNonClientFrameViewChromeOSTestApi 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(browser_view->immersive_mode_controller()->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(browser_view->immersive_mode_controller()->IsEnabled());
EXPECT_FALSE(browser_view->IsFullscreen());
EXPECT_TRUE(test_api.GetShouldPaint());
}
// Tests that caption buttons are hidden when entering tab fullscreen.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO(crbug.com/40276379): Reenable when bug is fixed.
#define MAYBE_CaptionButtonsHiddenNonImmersiveFullscreen \
DISABLED_CaptionButtonsHiddenNonImmersiveFullscreen
#else
#define MAYBE_CaptionButtonsHiddenNonImmersiveFullscreen \
CaptionButtonsHiddenNonImmersiveFullscreen
#endif
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip,
MAYBE_CaptionButtonsHiddenNonImmersiveFullscreen) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
content::WebContents* web_contents = browser_view->GetActiveWebContents();
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
EnterTabFullscreenMode(browser(), web_contents);
EXPECT_TRUE(browser_view->IsFullscreen());
EXPECT_FALSE(browser_view->immersive_mode_controller()->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(browser_view->immersive_mode_controller()->IsEnabled());
EXPECT_FALSE(browser_view->IsFullscreen());
// Caption button container visible again.
EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
}
// There should be no top inset when using the WebUI tab strip since the frame
// is invisible. Regression test for crbug.com/1076675
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTestWithWebUiTabStrip,
TopInset) {
// This test doesn't make sense in non-touch mode since it expects the WebUI
// tab strip to be active. This test is instantiated with and without touch
// mode.
if (!ui::TouchUiController::Get()->touch_ui()) {
return;
}
BrowserView* const browser_view =
BrowserView::GetBrowserViewForBrowser(browser());
EXPECT_EQ(0, GetFrameViewChromeOS(browser_view)->GetTopInset(false));
EnterOverviewMode();
EXPECT_EQ(0, GetFrameViewChromeOS(browser_view)->GetTopInset(false));
ExitOverviewMode();
EXPECT_EQ(0, GetFrameViewChromeOS(browser_view)->GetTopInset(false));
}
// Tests to ensure caption buttons are not painted when the WebUI tab strip is
// present for the browser window (crbug.com/1362731).
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTestWithWebUiTabStrip,
CaptionButtonsHiddenWhenUsingWebUITabStrip) {
auto* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
auto* frame_view = GetFrameViewChromeOS(browser_view);
if (ui::TouchUiController::Get()->touch_ui()) {
EXPECT_TRUE(browser_view->webui_tab_strip());
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
} else {
EXPECT_FALSE(browser_view->webui_tab_strip());
EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
}
}
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest,
IncognitoMarkedAsAssistantBlocked) {
Browser* incognito_browser = CreateIncognitoBrowser();
EXPECT_TRUE(incognito_browser->window()->GetNativeWindow()->GetProperty(
chromeos::kBlockedForAssistantSnapshotKey));
}
// Tests that browser frame minimum size constraint is updated in response to
// browser view layout.
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest,
FrameMinSizeIsUpdated) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* 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/827444).
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest,
RestoreMinimizedBrowserUpdatesCaption) {
// Enable session service.
SessionStartupPref pref(SessionStartupPref::LAST);
Profile* profile = browser()->profile();
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()->window()->Minimize();
CloseBrowserSynchronously(browser());
chrome::NewEmptyWindow(profile);
SessionRestoreTestHelper().Wait();
Browser* new_browser = BrowserList::GetInstance()->GetLastActive();
// Check that a layout occurs.
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(new_browser);
views::Widget* widget = browser_view->GetWidget();
BrowserNonClientFrameViewChromeOS* frame_view =
static_cast<BrowserNonClientFrameViewChromeOS*>(
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 WebAppNonClientFrameViewChromeOSTest
: public TopChromeMdParamTest<ChromeOSBrowserUITest> {
public:
WebAppNonClientFrameViewChromeOSTest() = default;
WebAppNonClientFrameViewChromeOSTest(
const WebAppNonClientFrameViewChromeOSTest&) = delete;
WebAppNonClientFrameViewChromeOSTest& operator=(
const WebAppNonClientFrameViewChromeOSTest&) = delete;
~WebAppNonClientFrameViewChromeOSTest() override = default;
GURL GetAppURL() const {
return https_server_.GetURL("app.com", "/ssl/google.html");
}
static SkColor GetThemeColor() { return SK_ColorBLUE; }
raw_ptr<Browser, 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()->profile(), std::move(web_app_info));
content::TestNavigationObserver navigation_observer(GetAppURL());
navigation_observer.StartWatchingNewWebContents();
app_browser_ = web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
navigation_observer.WaitForNavigationFinished();
#if BUILDFLAG(IS_CHROMEOS_LACROS)
ASSERT_TRUE(browser_test_util::WaitForWindowCreation(app_browser_));
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
browser_view_ = BrowserView::GetBrowserViewForBrowser(app_browser_);
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view_);
frame_header_ = static_cast<chromeos::DefaultFrameHeader*>(
BrowserNonClientFrameViewChromeOSTestApi(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_ = web_app_frame_toolbar_->GetAppMenuButton();
}
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_;
}
PageActionIconView* GetPageActionIcon(PageActionIconType type) {
return browser_view_->toolbar_button_provider()->GetPageActionIconView(
type);
}
ContentSettingImageView* GrantGeolocationPermission() {
content::RenderFrameHost* frame = app_browser_->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame();
content_settings::PageSpecificContentSettings* content_settings =
content_settings::PageSpecificContentSettings::GetForFrame(frame);
content_settings->OnContentAllowed(ContentSettingsType::GEOLOCATION);
return *base::ranges::find(*content_setting_views_,
ContentSettingImageModel::ImageType::GEOLOCATION,
&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:
// 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(WebAppNonClientFrameViewChromeOSTest,
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::GetScreen()
->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(WebAppNonClientFrameViewChromeOSTest, FocusableViews) {
SetUpWebApp();
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
browser_view_->GetFocusManager()->AdvanceFocus(false);
ASSERT_TRUE(WaitForFocus(true, web_app_menu_button_));
browser_view_->GetFocusManager()->AdvanceFocus(false);
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
ButtonVisibilityInOverviewMode) {
SetUpWebApp();
ASSERT_TRUE(WaitForVisible(true, web_app_frame_toolbar_));
EnterOverviewMode();
views::test::RunScheduledLayout(browser_view_);
ASSERT_TRUE(WaitForVisible(false, web_app_frame_toolbar_));
ExitOverviewMode();
views::test::RunScheduledLayout(browser_view_);
ASSERT_TRUE(WaitForVisible(true, web_app_frame_toolbar_));
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
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(WebAppNonClientFrameViewChromeOSTest, 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(WebAppNonClientFrameViewChromeOSTest,
IsToolbarButtonProvider) {
SetUpWebApp();
EXPECT_EQ(browser_view_->toolbar_button_provider(), web_app_frame_toolbar_);
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
ShowManagePasswordsIcon) {
SetUpWebApp();
content::WebContents* web_contents =
app_browser_->tab_strip_model()->GetActiveWebContents();
PageActionIconView* manage_passwords_icon =
GetPageActionIcon(PageActionIconType::kManagePasswords);
EXPECT_TRUE(manage_passwords_icon);
EXPECT_FALSE(manage_passwords_icon->GetVisible());
password_manager::PasswordForm password_form;
password_form.username_value = u"test";
password_form.url = GetAppURL().DeprecatedGetOriginAsURL();
password_form.match_type = password_manager::PasswordForm::MatchType::kExact;
std::vector<password_manager::PasswordForm> forms = {password_form};
PasswordsClientUIDelegateFromWebContents(web_contents)
->OnPasswordAutofilled(forms, url::Origin::Create(password_form.url), {});
chrome::ManagePasswordsForPage(app_browser_);
ASSERT_TRUE(WaitForVisible(true, manage_passwords_icon));
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest, ShowZoomIcon) {
SetUpWebApp();
content::WebContents* web_contents =
app_browser_->tab_strip_model()->GetActiveWebContents();
zoom::ZoomController* zoom_controller =
zoom::ZoomController::FromWebContents(web_contents);
PageActionIconView* zoom_icon = GetPageActionIcon(PageActionIconType::kZoom);
EXPECT_TRUE(zoom_icon);
EXPECT_FALSE(zoom_icon->GetVisible());
EXPECT_FALSE(ZoomBubbleView::GetZoomBubble());
zoom_controller->SetZoomLevel(blink::ZoomFactorToZoomLevel(1.5));
ASSERT_TRUE(WaitForVisible(true, zoom_icon));
EXPECT_TRUE(ZoomBubbleView::GetZoomBubble());
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest, ShowFindIcon) {
SetUpWebApp();
PageActionIconView* find_icon = GetPageActionIcon(PageActionIconType::kFind);
EXPECT_TRUE(find_icon);
EXPECT_FALSE(find_icon->GetVisible());
chrome::Find(app_browser_);
ASSERT_TRUE(WaitForVisible(true, find_icon));
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
ShowTranslateIcon) {
SetUpWebApp();
PageActionIconView* translate_icon =
GetPageActionIcon(PageActionIconType::kTranslate);
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);
ASSERT_TRUE(WaitForVisible(true, translate_icon));
}
// Tests that the focus toolbar command focuses the app menu button in web-app
// windows.
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
BrowserCommandFocusToolbarAppMenu) {
SetUpWebApp();
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
EXPECT_FALSE(web_app_menu_button_->HasFocus());
chrome::ExecuteCommand(app_browser_, IDC_FOCUS_TOOLBAR);
ASSERT_TRUE(WaitForFocus(true, web_app_menu_button_));
}
// 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(WebAppNonClientFrameViewChromeOSTest,
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();
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
EXPECT_FALSE(web_app_menu_button_->HasFocus());
EXPECT_FALSE(geolocation_icon->HasFocus());
chrome::ExecuteCommand(app_browser_, IDC_FOCUS_TOOLBAR);
ASSERT_TRUE(WaitForFocus(true, geolocation_icon));
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(WebAppNonClientFrameViewChromeOSTest,
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(WebAppNonClientFrameViewChromeOSTest,
BrowserCommandFocusNextPane) {
SetUpWebApp();
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
EXPECT_FALSE(web_app_menu_button_->HasFocus());
chrome::ExecuteCommand(app_browser_, IDC_FOCUS_NEXT_PANE);
ASSERT_TRUE(WaitForFocus(true, web_app_menu_button_));
}
// Tests the app icon and title are not shown.
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
IconShownAndTitleNotShown) {
SetUpWebApp();
auto* browser_view = BrowserView::GetBrowserViewForBrowser(app_browser_);
EXPECT_FALSE(browser_view->ShouldShowWindowIcon());
EXPECT_FALSE(browser_view->ShouldShowWindowTitle());
}
// Tests that the custom tab bar is focusable from the keyboard.
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
CustomTabBarIsFocusable) {
SetUpWebApp();
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
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);
ASSERT_TRUE(WaitForFocus(true, web_app_menu_button_));
EXPECT_FALSE(custom_tab_bar->close_button_for_testing()->HasFocus());
chrome::ExecuteCommand(app_browser_, IDC_FOCUS_NEXT_PANE);
ASSERT_TRUE(WaitForFocus(true, custom_tab_bar->close_button_for_testing()));
}
// Tests that the focus previous pane command focuses the app menu for web-app
// windows.
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
BrowserCommandFocusPreviousPane) {
SetUpWebApp();
ASSERT_TRUE(WaitForFocus(true, browser_view_->contents_web_view()));
EXPECT_FALSE(web_app_menu_button_->HasFocus());
chrome::ExecuteCommand(app_browser_, IDC_FOCUS_PREVIOUS_PANE);
ASSERT_TRUE(WaitForFocus(true, web_app_menu_button_));
}
// Tests that a web app's content settings icons can be interacted with.
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
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/839955
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
ActiveStateOfButtonMatchesWidget) {
SetUpWebApp();
chromeos::FrameCaptionButtonContainerView::TestApi test(
GetFrameViewChromeOS(browser_view_)->caption_button_container());
EXPECT_TRUE(WaitForPaintAsActive(true, test.size_button()));
EXPECT_TRUE(GetPaintingAsActive());
DeactivateWidget(browser_view_->GetWidget());
EXPECT_TRUE(WaitForPaintAsActive(false, test.size_button()));
EXPECT_FALSE(GetPaintingAsActive());
}
IN_PROC_BROWSER_TEST_P(WebAppNonClientFrameViewChromeOSTest,
PopupHasNoToolbar) {
SetUpWebApp();
Browser* popup_browser;
{
NavigateParams navigate_params(app_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();
popup_browser = navigate_params.browser;
}
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(popup_browser);
EXPECT_FALSE(browser_view->web_app_frame_toolbar_for_testing() &&
browser_view->web_app_frame_toolbar_for_testing()->GetVisible());
}
// Test the normal type browser's kTopViewInset is always 0.
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest, TopViewInset) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
ImmersiveModeController* immersive_mode_controller =
browser_view->immersive_mode_controller();
aura::Window* window = browser()->window()->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 always hidden in
// tablet mode.
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest,
BrowserHeaderVisibilityInTabletModeTest) {
if (!IsSnapWindowSupported()) {
GTEST_SKIP() << "Ash is too old.";
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
views::Widget* widget = browser_view->GetWidget();
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
widget->GetNativeWindow()->SetProperty(
aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorCanMaximize |
aura::client::kResizeBehaviorCanResize);
// Caption buttons are not supported when using the WebUI tab strip.
if (browser_view->webui_tab_strip()) {
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
} else {
EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
}
EnterOverviewMode();
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
ExitOverviewMode();
// Caption buttons are not supported when using the WebUI tab strip.
if (browser_view->webui_tab_strip()) {
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
} else {
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(), crosapi::mojom::SnapPosition::kPrimary);
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
}
// Regression test for https://crbug.com/879851.
// 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(BrowserNonClientFrameViewChromeOSTest, AppFrameColor) {
Browser* app_browser =
CreateBrowserForApp("test_browser_app", browser()->profile());
aura::Window* window = app_browser->window()->GetNativeWindow();
SkColor active_frame_color =
window->GetProperty(chromeos::kFrameActiveColorKey);
const bool is_dark_mode_state =
BrowserView::GetBrowserViewForBrowser(browser())
->GetNativeTheme()
->ShouldUseDarkColors();
EXPECT_EQ(active_frame_color, is_dark_mode_state
? gfx::kGoogleGrey900
: SkColorSetRGB(0xFF, 0xFF, 0xFF))
<< "RGB: " << SkColorGetR(active_frame_color) << ", "
<< SkColorGetG(active_frame_color) << ", "
<< SkColorGetB(active_frame_color);
}
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest,
AccessibleProperties) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* 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 PreventCloseBrowserNonClientFrameViewChromeOSTest
: public PreventCloseTestBase {
public:
PreventCloseBrowserNonClientFrameViewChromeOSTest() = default;
PreventCloseBrowserNonClientFrameViewChromeOSTest(
const PreventCloseBrowserNonClientFrameViewChromeOSTest&) = delete;
PreventCloseBrowserNonClientFrameViewChromeOSTest& operator=(
const PreventCloseBrowserNonClientFrameViewChromeOSTest&) = delete;
~PreventCloseBrowserNonClientFrameViewChromeOSTest() override = default;
void SetUpOnMainThread() override {
PreventCloseTestBase::SetUpOnMainThread();
#if BUILDFLAG(IS_CHROMEOS_LACROS)
loopback_crosapi_ =
std::make_unique<web_app::LoopbackCrosapiAppServiceProxy>(profile());
#endif
}
void TearDownOnMainThread() override {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
loopback_crosapi_ = nullptr;
#endif
PreventCloseTestBase::TearDownOnMainThread();
}
views::Button* GetWindowCloseButton(Browser* 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();
}
private:
#if BUILDFLAG(IS_CHROMEOS_LACROS)
std::unique_ptr<web_app::LoopbackCrosapiAppServiceProxy> loopback_crosapi_;
#endif
};
IN_PROC_BROWSER_TEST_F(PreventCloseBrowserNonClientFrameViewChromeOSTest,
CloseButtonIsDisabled) {
InstallPWA(GURL(kCalculatorAppUrl), web_app::kCalculatorAppId);
SetPoliciesAndWaitUntilInstalled(web_app::kCalculatorAppId,
kPreventCloseEnabledForCalculator,
kCalculatorForceInstalled);
Browser* const browser =
LaunchPWA(web_app::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->profile(), web_app::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(PreventCloseBrowserNonClientFrameViewChromeOSTest,
CloseButtonIsEnabled) {
InstallPWA(GURL(kCalculatorAppUrl), web_app::kCalculatorAppId);
Browser* const browser =
LaunchPWA(web_app::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(BrowserNonClientFrameViewChromeOSTest,
ImmersiveModeTopViewInset) {
Browser* app_browser =
CreateBrowserForApp("test_browser_app", browser()->profile());
// TODO(neis): Move this into the CreateBrowser* functions.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
ui_test_utils::CreateAsyncWidgetRequestWaiter(*browser()).Wait();
#endif
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(app_browser);
ImmersiveModeController* immersive_mode_controller =
browser_view->immersive_mode_controller();
aura::Window* window = app_browser->window()->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(BrowserNonClientFrameViewChromeOSTest,
ToggleTabletModeWhileImmersiveModeEnabled) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
ImmersiveModeController* immersive_mode_controller =
browser_view->immersive_mode_controller();
EnterImmersiveFullscreenMode(browser());
// Should exit immersive mode + fullscreen when tablet mode is enabled.
EnterTabletMode();
ImmersiveModeTester(browser()).WaitForFullscreenToExit();
EXPECT_FALSE(immersive_mode_controller->IsEnabled());
EXPECT_FALSE(browser_view->IsFullscreen());
}
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewChromeOSTest,
ToggleImmersiveModeWhileTabletModeEnabled) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
ImmersiveModeController* immersive_mode_controller =
browser_view->immersive_mode_controller();
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());
}
// TODO(b/270175923): Consider using WebUiTabStripOverrideTest, since it
// makes sense for it to always be enabled.
using FloatBrowserNonClientFrameViewChromeOSTest =
TopChromeMdParamTest<ChromeOSBrowserUITest>;
// TODO(crbug.com/40286309): Port this test to Lacros.
#if BUILDFLAG(IS_CHROMEOS_ASH)
IN_PROC_BROWSER_TEST_P(FloatBrowserNonClientFrameViewChromeOSTest,
TabletModeMultitaskMenu) {
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::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()));
ASSERT_TRUE(WaitForFocus(true, omnibox));
// 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();
ASSERT_TRUE(WaitForFocus(false, omnibox));
auto* multitask_menu_event_handler =
ash::TabletModeControllerTestApi()
.tablet_mode_window_manager()
->tablet_mode_multitask_menu_controller();
EXPECT_TRUE(multitask_menu_event_handler->multitask_menu());
if (browser_view->webui_tab_strip()) {
// The tab strip doesn't get shown if the menu is.
ASSERT_FALSE(browser_view->webui_tab_strip()->GetVisible());
}
// Tap on the omnibox outside the menu takes focus and closes the menu.
ASSERT_NO_FATAL_FAILURE(
event_generator.GestureTapAt(omnibox_bounds.left_center()));
ASSERT_TRUE(WaitForFocus(true, omnibox));
EXPECT_FALSE(multitask_menu_event_handler->multitask_menu());
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
IN_PROC_BROWSER_TEST_P(FloatBrowserNonClientFrameViewChromeOSTest,
BrowserHeaderVisibilityInTabletModeTest) {
if (!IsSnapWindowSupported()) {
GTEST_SKIP() << "Ash is too old.";
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
EnterTabletMode();
ASSERT_TRUE(WaitForVisible(false, frame_view->caption_button_container()));
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, crosapi::mojom::SnapPosition::kSecondary);
ASSERT_TRUE(WaitForVisible(false, frame_view->caption_button_container()));
EXPECT_FALSE(immersive_controller->IsEnabled());
// Float the window; the title bar becomes visible.
chromeos::FloatControllerBase::Get()->SetFloat(
window, chromeos::FloatStartLocation::kBottomRight);
ASSERT_TRUE(WaitForVisible(true, frame_view->caption_button_container()));
EXPECT_FALSE(immersive_controller->IsEnabled());
}
// Test that for a browser app window, its caption buttons may or may not hide
// in tablet mode.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO(crbug.com/40946296): Finish porting to Lacros when the bug is fixed.
#define MAYBE_BrowserAppHeaderVisibilityInTabletModeTest \
DISABLED_BrowserAppHeaderVisibilityInTabletModeTest
#else
#define MAYBE_BrowserAppHeaderVisibilityInTabletModeTest \
BrowserAppHeaderVisibilityInTabletModeTest
#endif
IN_PROC_BROWSER_TEST_P(FloatBrowserNonClientFrameViewChromeOSTest,
MAYBE_BrowserAppHeaderVisibilityInTabletModeTest) {
if (!IsSnapWindowSupported()) {
GTEST_SKIP() << "Ash is too old.";
}
Browser* browser2 =
CreateBrowserForApp("test_browser_app", browser()->profile());
BrowserView* browser_view2 = BrowserView::GetBrowserViewForBrowser(browser2);
views::Widget* widget2 = browser_view2->GetWidget();
BrowserNonClientFrameViewChromeOS* 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(immersive_controller->IsEnabled());
// Snap a window. Immersive mode is enabled so its title bar is not visible.
SnapWindow(widget2->GetNativeWindow(),
crosapi::mojom::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(FloatBrowserNonClientFrameViewChromeOSTest,
ToggleMultitaskMenu) {
EXPECT_TRUE(chrome::IsCommandEnabled(browser(), IDC_TOGGLE_MULTITASK_MENU));
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* 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);
ASSERT_TRUE(base::test::RunUntil(
[&]() { return 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);
}
ASSERT_TRUE(base::test::RunUntil(
[&]() { return !size_button->IsMultitaskMenuShown(); }));
}
using HomeLauncherBrowserNonClientFrameViewChromeOSTest =
BrowserNonClientFrameViewChromeOSTest;
IN_PROC_BROWSER_TEST_P(HomeLauncherBrowserNonClientFrameViewChromeOSTest,
TabletModeBrowserCaptionButtonVisibility) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
// Caption buttons are not supported when using the WebUI tab strip.
if (browser_view->webui_tab_strip()) {
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
} else {
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();
// Caption buttons are not supported when using the WebUI tab strip.
if (browser_view->webui_tab_strip()) {
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
} else {
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
// |BrowserNonClientFrameViewChromeOS|.
IN_PROC_BROWSER_TEST_P(HomeLauncherBrowserNonClientFrameViewChromeOSTest,
CaptionButtonVisibilityForBrowserLaunchedInTabletMode) {
EnterTabletMode();
auto* new_browser = CreateBrowser(browser()->profile());
auto* frame_view =
GetFrameViewChromeOS(BrowserView::GetBrowserViewForBrowser(new_browser));
EXPECT_FALSE(frame_view->caption_button_container()->GetVisible());
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO(crbug.com/40946296): Finish porting to Lacros when the bug is fixed.
#define MAYBE_TabletModeAppCaptionButtonVisibility \
DISABLED_TabletModeAppCaptionButtonVisibility
#else
#define MAYBE_TabletModeAppCaptionButtonVisibility \
TabletModeAppCaptionButtonVisibility
#endif
IN_PROC_BROWSER_TEST_P(HomeLauncherBrowserNonClientFrameViewChromeOSTest,
MAYBE_TabletModeAppCaptionButtonVisibility) {
Browser* app_browser =
CreateBrowserForApp("test_browser_app", browser()->profile());
BrowserView* browser_view =
BrowserView::GetBrowserViewForBrowser(app_browser);
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
EXPECT_TRUE(frame_view->caption_button_container()->GetVisible());
ImmersiveModeController* immersive_mode_controller =
browser_view->immersive_mode_controller();
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 LockedFullscreenBrowserNonClientFrameViewChromeOSTest =
TopChromeMdParamTest<ChromeOSBrowserUITest>;
IN_PROC_BROWSER_TEST_P(LockedFullscreenBrowserNonClientFrameViewChromeOSTest,
ToggleTabletMode) {
if (!IsIsShelfVisibleSupported()) {
GTEST_SKIP() << "Ash is too old.";
}
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
EXPECT_FALSE(browser_view->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(browser_view->immersive_mode_controller()->IsEnabled());
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// TODO(crbug.com/40276379): Enable this assertion once the bug is fixed (at
// the moment PinWindow returns too early).
#else
EXPECT_FALSE(IsShelfVisible());
#endif
auto* widget = browser_view->GetWidget();
auto* immersive_controller = chromeos::ImmersiveFullscreenController::Get(
views::Widget::GetWidgetForNativeView(widget->GetNativeWindow()));
EXPECT_FALSE(immersive_controller->IsEnabled());
EnterTabletMode();
EXPECT_TRUE(browser_view->GetWidget()->IsFullscreen());
EXPECT_FALSE(browser_view->immersive_mode_controller()->IsEnabled());
EXPECT_FALSE(IsShelfVisible());
}
// The remaining tests make sense only for Ash, not for Lacros.
#if BUILDFLAG(IS_CHROMEOS_ASH)
using BrowserNonClientFrameViewAshTestNoWebUiTabStrip =
BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip;
// Tests that Avatar icon should show on the top left corner of the teleported
// browser window on ChromeOS.
// TODO(http://crbug.com/1059514): This test should be made to work with the
// webUI tabstrip.
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewAshTestNoWebUiTabStrip,
AvatarDisplayOnTeleportedWindow) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
BrowserNonClientFrameViewChromeOS* frame_view =
GetFrameViewChromeOS(browser_view);
BrowserNonClientFrameViewChromeOSTestApi test_api(frame_view);
aura::Window* window = browser()->window()->GetNativeWindow();
EXPECT_FALSE(MultiUserWindowManagerHelper::ShouldShowAvatar(window));
EXPECT_FALSE(test_api.GetProfileIndicatorIcon());
const AccountId account_id1 =
multi_user_util::GetAccountIdFromProfile(browser()->profile());
TestMultiUserWindowManager* window_manager =
TestMultiUserWindowManager::Create(browser(), account_id1);
// Teleport the window to another desktop.
const AccountId account_id2(AccountId::FromUserEmail("user2"));
window_manager->ShowWindowForUser(window, account_id2);
EXPECT_TRUE(MultiUserWindowManagerHelper::ShouldShowAvatar(window));
EXPECT_TRUE(test_api.GetProfileIndicatorIcon());
// Teleport the window back to owner desktop.
window_manager->ShowWindowForUser(window, account_id1);
EXPECT_FALSE(MultiUserWindowManagerHelper::ShouldShowAvatar(window));
EXPECT_FALSE(test_api.GetProfileIndicatorIcon());
}
using BrowserNonClientFrameViewAshTest = BrowserNonClientFrameViewChromeOSTest;
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewAshTest,
SettingsSystemWebAppHasMinimumWindowSize) {
// Install the Settings System Web App.
ash::SystemWebAppManager::GetForTest(browser()->profile())
->InstallSystemAppsForTesting();
// Open a settings window.
ui_test_utils::BrowserChangeObserver observer(
nullptr, ui_test_utils::BrowserChangeObserver::ChangeType::kAdded);
auto* settings_manager = chrome::SettingsWindowManager::GetInstance();
settings_manager->ShowOSSettings(browser()->profile());
observer.Wait();
Browser* settings_browser =
settings_manager->FindBrowserForProfile(browser()->profile());
// Try to set the bounds to a tiny value.
settings_browser->window()->SetBounds(gfx::Rect(1, 1));
// The window has a reasonable size.
gfx::Rect actual_bounds = settings_browser->window()->GetBounds();
EXPECT_LE(300, actual_bounds.width());
EXPECT_LE(100, actual_bounds.height());
}
// Enumeration of test modes for
// `BrowserNonClientFrameViewAshThemeChangeTest`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 BrowserNonClientFrameViewAshThemeChangeTest
: public InProcessBrowserTest,
public testing::WithParamInterface<
std::tuple<ThemeChangeTestMode,
/*should_animate_theme_changes=*/bool>> {
public:
BrowserNonClientFrameViewAshThemeChangeTest() {
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());
delegate->SetShouldAnimateThemeChanges(ShouldAnimateThemeChanges());
// 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 std::get<0>(GetParam());
}
// Returns whether theme changes should be animated for the web app under test
// given test parameterization.
bool ShouldAnimateThemeChanges() const { return std::get<1>(GetParam()); }
// Toggles the color mode, triggering propagation of theme change events.
void ToggleColorMode() {
auto* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
native_theme->set_use_dark_colors(!native_theme->ShouldUseDarkColors());
native_theme->set_preferred_color_scheme(
native_theme->CalculatePreferredColorScheme());
native_theme->NotifyOnNativeThemeUpdated();
}
// 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()->profile(); }
private:
std::unique_ptr<ash::TestSystemWebAppInstallation>
system_web_app_installation_;
std::unique_ptr<net::EmbeddedTestServer> test_server_;
};
INSTANTIATE_TEST_SUITE_P(
Mode,
BrowserNonClientFrameViewAshThemeChangeTest,
testing::Combine(testing::Values(ThemeChangeTestMode::kSWA,
ThemeChangeTestMode::kNonSWA),
/*should_animate_theme_changes=*/testing::Bool()),
[](const testing::TestParamInfo<
std::tuple<ThemeChangeTestMode,
/*should_animate_theme_changes=*/bool>>& info) {
ThemeChangeTestMode test_mode = std::get<0>(info.param);
bool should_animate_theme_changes = std::get<1>(info.param);
std::stringstream name;
switch (test_mode) {
case ThemeChangeTestMode::kSWA:
name << "kSWA";
break;
case ThemeChangeTestMode::kNonSWA:
name << "kNonSWA";
break;
}
if (should_animate_theme_changes) {
name << "_ShouldAnimateThemeChanges";
}
return name.str();
});
IN_PROC_BROWSER_TEST_P(BrowserNonClientFrameViewAshThemeChangeTest,
ThemeChange) {
// Skip test parameterizations for non-system web apps that don't make sense.
if (GetThemeChangeTestMode() == ThemeChangeTestMode::kNonSWA &&
ShouldAnimateThemeChanges()) {
GTEST_SKIP();
}
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);
Browser* browser = chrome::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->GetBackground()->get_color(),
browser->app_controller()->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->GetBackground()->get_color(),
browser->app_controller()->GetBackgroundColor().value());
EXPECT_EQ(contents_web_view->GetBackground()->get_color(),
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->GetBackground()->get_color(),
browser->app_controller()->GetBackgroundColor().value());
EXPECT_EQ(contents_web_view->GetBackground()->get_color(),
web_contents->GetBackgroundColor().value());
// If theme changes should be animated, the layer associated with the
// `contents_web_view` native view should be immediately hidden.
auto* layer = contents_web_view->holder()->native_view()->layer();
if (ShouldAnimateThemeChanges()) {
EXPECT_EQ(layer->GetTargetOpacity(), std::nextafter(0.f, 1.f));
} else {
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->GetBackground()->get_color(),
browser->app_controller()->GetBackgroundColor().value());
EXPECT_EQ(contents_web_view->GetBackground()->get_color(),
web_contents->GetBackgroundColor().value());
}
// If theme changes should be animated, the layer associated with the
// `contents_web_view` native view should be animated back in only after a
// round trip through the renderer and compositor pipelines. This should
// ensure that the web contents has finished repainting theme changes.
if (ShouldAnimateThemeChanges()) {
base::test::TestFuture<bool> visual_state_change_future;
web_contents->GetRenderViewHost()->GetWidget()->InsertVisualStateCallback(
visual_state_change_future.GetCallback());
EXPECT_TRUE(visual_state_change_future.Wait());
EXPECT_EQ(layer->GetTargetOpacity(), 1.f);
}
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#define INSTANTIATE_TEST_SUITE(name) \
INSTANTIATE_TEST_SUITE_P(All, name, ::testing::Values(false, true))
INSTANTIATE_TEST_SUITE(BrowserNonClientFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(BrowserNonClientFrameViewChromeOSTestNoWebUiTabStrip);
INSTANTIATE_TEST_SUITE(BrowserNonClientFrameViewChromeOSTestWithWebUiTabStrip);
INSTANTIATE_TEST_SUITE(FloatBrowserNonClientFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(HomeLauncherBrowserNonClientFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(LockedFullscreenBrowserNonClientFrameViewChromeOSTest);
INSTANTIATE_TEST_SUITE(WebAppNonClientFrameViewChromeOSTest);
#if BUILDFLAG(IS_CHROMEOS_ASH)
INSTANTIATE_TEST_SUITE(BrowserNonClientFrameViewAshTestNoWebUiTabStrip);
INSTANTIATE_TEST_SUITE(BrowserNonClientFrameViewAshTest);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)