blob: b6e94f4591dd175bd8d606cc7b077a97e823d13e [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/memory/memory_pressure_monitor.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/buildflag.h"
#include "chrome/browser/background/glic/glic_controller.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/glic/fre/glic_fre_controller.h"
#include "chrome/browser/glic/glic_metrics.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/glic_profile_manager.h"
#include "chrome/browser/glic/host/glic.mojom.h"
#include "chrome/browser/glic/public/glic_keyed_service_factory.h"
#include "chrome/browser/glic/test_support/glic_test_util.h"
#include "chrome/browser/glic/test_support/interactive_glic_test.h"
#include "chrome/browser/glic/test_support/interactive_test_util.h"
#include "chrome/browser/glic/widget/glic_view.h"
#include "chrome/browser/glic/widget/glic_window_controller.h"
#include "chrome/browser/glic/widget/glic_window_controller_impl.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/tabs/glic_button.h"
#include "chrome/browser/ui/views/tabs/tab_strip_action_container.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "ui/base/test/ui_controls.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/test/virtual_display_util.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/interaction_test_util_views.h"
#include "ui/views/interaction/widget_focus_observer.h"
#include "ui/views/test/button_test_api.h"
namespace glic {
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kFirstTab);
const InteractiveBrowserTestApi::DeepQuery kMockGlicClientHangButton = {
"#hang"};
} // anonymous namespace
class GlicWindowControllerUiTest : public test::InteractiveGlicTest {
public:
GlicWindowControllerUiTest() {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kGlicHostLogging);
features_.InitWithFeaturesAndParameters(
{{features::kTabstripComboButton, {}},
{features::kGlicActor, {}},
{features::kGlicActorUi,
{{features::kGlicActorUiTaskIconName, "true"}}}},
{});
}
~GlicWindowControllerUiTest() override = default;
auto SimulateGlicHotkey() {
// TODO: Actually implement the hotkey when we know what it is.
return Do([this]() {
glic_service()->ToggleUI(nullptr, /*prevent_close=*/false,
mojom::InvocationSource::kOsHotkey);
});
}
auto SimulateOpenMenuItem() {
return Do([this]() {
glic_controller_->Show(mojom::InvocationSource::kOsButtonMenu);
});
}
auto SimulateOsButton() {
return Do([this]() {
glic_controller_->Toggle(mojom::InvocationSource::kOsButton);
});
}
auto ForceInvalidateAccount() {
return Do([this]() { InvalidateAccount(window_controller().profile()); });
}
auto ForceReauthAccount() {
return Do([this]() { ReauthAccount(window_controller().profile()); });
}
bool IsWorkAreaTooSmallForTest() {
gfx::Rect work_area_bounds =
display::Screen::Get()->GetPrimaryDisplay().work_area();
gfx::Size glic_expected_size = GlicWidget::GetInitialSize();
gfx::Size cell_size = {work_area_bounds.width() / 3,
work_area_bounds.height() / 3};
// Set browser bounds to the center cell of the work area bounds.
gfx::Rect browser_bounds = gfx::Rect(
gfx::Point(work_area_bounds.width() / 3 + work_area_bounds.x(),
work_area_bounds.height() / 3 + work_area_bounds.y()),
cell_size);
browser()->window()->SetBounds(browser_bounds);
browser_bounds = browser()->window()->GetBounds();
// The test places the browser in the center cell. For the test to be valid,
// there must be enough space around the browser for the GlicWidget to
// appear without being clipped or overlapping in a way that breaks the test
// logic.
return cell_size.width() <= glic_expected_size.width() / 2 ||
cell_size.height() <= glic_expected_size.height() / 2 ||
work_area_bounds.width() <=
browser_bounds.width() + glic_expected_size.width() ||
work_area_bounds.height() <=
browser_bounds.height() + glic_expected_size.height();
}
private:
std::unique_ptr<GlicController> glic_controller_ =
std::make_unique<GlicController>();
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, ShowAndCloseDetachedWidget) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
CloseGlicWindow(), CheckControllerHasWidget(false));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, DoNotCrashOnBrowserClose) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kAttached));
chrome::CloseAllBrowsers();
ui_test_utils::WaitForBrowserToClose();
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, DoNotCrashWhenReopening) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kAttached), CloseGlicWindow(),
OpenGlicWindow(GlicWindowMode::kAttached));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, ButtonTogglesGlicWindow) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached),
PressButton(kGlicButtonElementId),
InAnyContext(WaitForHide(kGlicViewElementId)),
CheckControllerHasWidget(false),
PressButton(kGlicButtonElementId),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, TaskIconTogglesGlicWindow) {
StartTaskAndShowActorTaskIcon();
RunTestSequence(
ObserveState(test::internal::kFloatyViewState, GetHostForActiveTab()),
OpenGlicWindow(GlicWindowMode::kDetached),
PressButton(kGlicActorTaskIconElementId),
WaitForState(test::internal::kFloatyViewState,
mojom::CurrentView::kActuation),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
PressButton(kGlicActorTaskIconElementId),
InAnyContext(WaitForHide(kGlicViewElementId)),
CheckControllerHasWidget(false));
}
IN_PROC_BROWSER_TEST_F(
GlicWindowControllerUiTest,
GlicButtonAndTaskIconButtonTogglesConversationAndActuationView) {
StartTaskAndShowActorTaskIcon();
RunTestSequence(
ObserveState(test::internal::kFloatyViewState, GetHostForActiveTab()),
OpenGlicWindow(GlicWindowMode::kDetached),
PressButton(kGlicActorTaskIconElementId),
WaitForState(test::internal::kFloatyViewState,
mojom::CurrentView::kActuation),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
PressButton(kGlicButtonElementId),
WaitForState(test::internal::kFloatyViewState,
mojom::CurrentView::kConversation),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
PressButton(kGlicActorTaskIconElementId),
WaitForState(test::internal::kFloatyViewState,
mojom::CurrentView::kActuation),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached));
}
constexpr char kActivateSurfaceIncompatibilityNotice[] =
"Programmatic window activation does not work on the Weston reference "
"implementation of Wayland used on Linux testbots. It also doesn't work "
"reliably on Linux in general. For this reason, some of these tests which "
"use ActivateSurface() may be skipped on machine configurations which do "
"not reliably support them.";
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
ButtonWhenAttachedToActiveBrowserCloses) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kAttached),
SetOnIncompatibleAction(OnIncompatibleAction::kSkipTest,
kActivateSurfaceIncompatibilityNotice),
ActivateSurface(kBrowserViewElementId),
// Glic should close.
PressButton(kGlicButtonElementId),
InAnyContext(WaitForHide(kGlicViewElementId)),
CheckControllerHasWidget(false));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
HotkeyWhenDetachedActiveCloses) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached),
SetOnIncompatibleAction(OnIncompatibleAction::kIgnoreAndContinue,
kActivateSurfaceIncompatibilityNotice),
InAnyContext(ActivateSurface(test::kGlicHostElementId)),
SimulateGlicHotkey(), InAnyContext(WaitForHide(kGlicViewElementId)),
CheckControllerHasWidget(false));
}
// TODO(393203136): Once tests can observe window controller state rather than
// polling, make a test like this one with glic initially attached.
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
HotkeyDetachedWithNotNormalBrowser) {
RunTestSequence(
Do([&]() {
Browser* const pwa =
CreateBrowserForApp("app name", browser()->profile());
pwa->window()->Activate();
}),
SimulateGlicHotkey(),
InAnyContext(WaitForShow(kGlicViewElementId).SetMustRemainVisible(false)),
CheckControllerWidgetMode(GlicWindowMode::kDetached));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
HotkeyOpensDetachedWithMinimizedBrowser) {
RunTestSequence(
// Glic should open attached to active browser.
SetOnIncompatibleAction(OnIncompatibleAction::kSkipTest,
kActivateSurfaceIncompatibilityNotice),
ActivateSurface(kBrowserViewElementId));
browser()->window()->Minimize();
ASSERT_TRUE(ui_test_utils::WaitForMinimized(browser()));
RunTestSequence(
SimulateGlicHotkey(),
InAnyContext(WaitForShow(kGlicViewElementId).SetMustRemainVisible(false)),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached));
}
#if BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
HotkeyOpensDetachedWithNonActiveBrowser) {
RunTestSequence(
// Glic should open attached to active browser.
SetOnIncompatibleAction(OnIncompatibleAction::kSkipTest,
kActivateSurfaceIncompatibilityNotice),
ActivateSurface(kBrowserViewElementId));
// This will make some other window the foreground window.
browser()->window()->Deactivate();
RunTestSequence(
SimulateGlicHotkey(),
InAnyContext(WaitForShow(kGlicViewElementId).SetMustRemainVisible(false)),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached));
}
#endif // BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
ESCWhenDetachedActiveCloses) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached),
SetOnIncompatibleAction(OnIncompatibleAction::kIgnoreAndContinue,
kActivateSurfaceIncompatibilityNotice),
InAnyContext(ActivateSurface(test::kGlicHostElementId)),
SimulateAcceleratorPress(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE)),
InAnyContext(WaitForHide(kGlicViewElementId)),
CheckControllerHasWidget(false));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
ESCWhenAttachedActiveCloses) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kAttached),
SetOnIncompatibleAction(OnIncompatibleAction::kIgnoreAndContinue,
kActivateSurfaceIncompatibilityNotice),
InAnyContext(ActivateSurface(test::kGlicHostElementId)),
SimulateAcceleratorPress(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE)),
InAnyContext(WaitForHide(kGlicViewElementId)),
CheckControllerHasWidget(false));
}
// TODO: Re-nable this test when there is a glic state for post-resize.
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
DISABLED_CloseWithContextMenu) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kAttached), CheckControllerHasWidget(true),
MoveMouseTo(kGlicViewElementId),
MayInvolveNativeContextMenu(
ClickMouse(ui_controls::RIGHT), WaitForHide(kBrowserViewElementId),
InAnyContext(
SelectMenuItem(RenderViewContextMenu::kGlicCloseMenuItem))),
CheckControllerHasWidget(false));
}
// Flaky on macOS: https://crbug.com/401158115
#if BUILDFLAG(IS_MAC)
#define MAYBE_OpenMenuItemShows DISABLED_OpenMenuItemShows
#else
#define MAYBE_OpenMenuItemShows OpenMenuItemShows
#endif
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, MAYBE_OpenMenuItemShows) {
RunTestSequence(SimulateOpenMenuItem(),
WaitForAndInstrumentGlic(kHostAndContents),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
CloseGlicWindow(), CheckControllerHasWidget(false));
}
#if BUILDFLAG(IS_WIN)
// On Windows, the OsButton toggles opening and closing floaty, because floaty
// will never be active when the os button is clicked.
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, OsButtonToggles) {
RunTestSequence(
SimulateOsButton(), WaitForAndInstrumentGlic(kHostAndContents),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached), SimulateOsButton(),
WaitForHide(test::kGlicHostElementId), CheckControllerHasWidget(false));
}
#endif // BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
OpenMenuItemWhenAttachedToActiveBrowserDoesNotClose) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kAttached),
// Glic should close.
SetOnIncompatibleAction(OnIncompatibleAction::kSkipTest,
kActivateSurfaceIncompatibilityNotice),
ActivateSurface(kBrowserViewElementId), SimulateOpenMenuItem(),
CheckControllerShowing(true));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
OpenMenuItemWhenDetachedActiveDoesNotClose) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached),
SetOnIncompatibleAction(OnIncompatibleAction::kIgnoreAndContinue,
kActivateSurfaceIncompatibilityNotice),
InAnyContext(ActivateSurface(test::kGlicHostElementId)),
SimulateOpenMenuItem(), CheckControllerShowing(true));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
OpeningProfilePickerClosesPanel) {
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kDetached),
CheckControllerWidgetMode(GlicWindowMode::kDetached), Do([&]() {
glic::GlicProfileManager::GetInstance()->ShowProfilePicker();
}),
CheckControllerHasWidget(false));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
ClientUnresponsiveThenError) {
base::HistogramTester histogram_tester;
RunTestSequence(
OpenGlicWindow(GlicWindowMode::kAttached),
ClickMockGlicElement(kMockGlicClientHangButton, true),
ObserveState(test::internal::kGlicAppState, GetHostForActiveTab()),
WaitForState(test::internal::kGlicAppState,
mojom::WebUiState::kUnresponsive),
// Client should show error after showing the unresponsive UI for 5s.
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kError));
histogram_tester.ExpectTotalCount(
"Glic.Host.WebClientUnresponsiveState.Duration", 1);
histogram_tester.ExpectTotalCount("Glic.Host.WebClientUnresponsiveState", 2);
// One sample in the WebClientUnresponsiveState.ENTERED_FROM_CUSTOM_HEARTBEAT
// (1) bucket.
histogram_tester.ExpectBucketCount("Glic.Host.WebClientUnresponsiveState", 1,
1);
// One sample in the WebClientUnresponsiveState.EXITED (4) bucket.
histogram_tester.ExpectBucketCount("Glic.Host.WebClientUnresponsiveState", 4,
1);
}
// ASAN builds are slow enough that the responsiveness check actually sometimes
// triggers just while setting this up, before we deactivate the window.
// Rather than adjust the timeouts to make this test even slower, just disable
// it for those builds (as well as similarly slow sanitizer builds).
#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
defined(MEMORY_SANITIZER)
#define MAYBE_ClientUnresponsiveWhileBrowserNotActive \
DISABLED_ClientUnresponsiveWhileBrowserNotActive
#else
#define MAYBE_ClientUnresponsiveWhileBrowserNotActive \
ClientUnresponsiveWhileBrowserNotActive
#endif
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
MAYBE_ClientUnresponsiveWhileBrowserNotActive) {
const base::TimeDelta kTimeToWait = base::Seconds(7);
ASSERT_GT(kTimeToWait,
base::Milliseconds(
features::kGlicClientResponsivenessCheckTimeoutMs.Get() +
features::kGlicClientResponsivenessCheckIntervalMs.Get()));
// This is another window to which we can give focus. It's not otherwise
// guaranteed that we can reliably make all relevant windows inactive
// (this is subtle and platform-specific, unfortunately).
auto other_widget = std::make_unique<views::Widget>();
base::HistogramTester histogram_tester;
RunTestSequence(
ObserveState(test::internal::kGlicAppState, GetHostForActiveTab()),
OpenGlicWindow(GlicWindowMode::kAttached),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kReady),
ObserveState(views::test::kCurrentWidgetFocus),
WithView(kBrowserViewElementId,
[&](BrowserView* browser_view) {
views::Widget::InitParams params{
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW};
params.bounds = gfx::Rect(0, 0, 200, 200);
params.context = browser_view->GetWidget()->GetNativeWindow();
other_widget->Init(std::move(params));
other_widget->Show();
browser_view->GetWidget()->Deactivate();
other_widget->Activate();
}),
WaitForState(views::test::kCurrentWidgetFocus, other_widget.get()),
// This click dispatches via JavaScript and doesn't change focus.
ClickMockGlicElement(kMockGlicClientHangButton, true), Wait(kTimeToWait),
CheckState(test::internal::kGlicAppState, mojom::WebUiState::kReady),
Do([&] {
histogram_tester.ExpectTotalCount(
"Glic.Host.WebClientUnresponsiveState", 0);
}),
Do([&] { browser()->window()->Activate(); }),
WaitForState(test::internal::kGlicAppState,
mojom::WebUiState::kUnresponsive),
Do([&] {
// ENTERED_FROM_CUSTOM_HEARTBEAT (1)
histogram_tester.ExpectBucketCount(
"Glic.Host.WebClientUnresponsiveState", 1, 1);
}));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
InvalidatedAccountWhileLoadingGlic) {
RunTestSequence(
ObserveState(test::internal::kGlicAppState, GetHostForActiveTab()),
SimulateGlicHotkey(), CheckControllerHasWidget(true),
ForceInvalidateAccount(), WaitForAndInstrumentGlic(kHostOnly),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kSignIn),
InAnyContext(ClickElement(test::kGlicHostElementId, {"#signInButton"},
ui_controls::LEFT, ui_controls::kNoAccelerator,
ExecuteJsMode::kFireAndForget)),
WaitForHide(test::kGlicHostElementId),
// Without a pause here, we will 'sign-in' before the callback is
// registered to listen for it. This isn't a bug because it takes real
// users finite time to actually sign-in.
Wait(base::Milliseconds(500)), ForceReauthAccount(),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kReady));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
InvalidatedAccountSignInOnGlicOpenFlow) {
RunTestSequence(
ObserveState(test::internal::kGlicAppState, GetHostForActiveTab()),
ForceInvalidateAccount(), SimulateGlicHotkey(),
CheckControllerHasWidget(false), InstrumentTab(kFirstTab),
WaitForWebContentsReady(kFirstTab),
// Without a pause here, we will 'sign-in' before the callback is
// registered to listen for it. This isn't a bug because it takes real
// users finite time to actually sign-in.
Wait(base::Milliseconds(500)), ForceReauthAccount(),
WaitForAndInstrumentGlic(kHostOnly),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kReady));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
AccountInvalidatedWhileGlicOpen) {
RunTestSequence(
SimulateGlicHotkey(), CheckControllerHasWidget(true),
ObserveState(test::internal::kGlicAppState, GetHostForActiveTab()),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kReady),
ForceInvalidateAccount(),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kSignIn),
ForceReauthAccount(),
WaitForState(test::internal::kGlicAppState, mojom::WebUiState::kReady));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest,
DetachedWidgetIsTrackedByOcclusionTracker) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached),
CheckOcclusionTracked(true));
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, TestInitialBounds) {
// The GlicButton and Tabstrip are not actually shown until a tab is created.
chrome::AddTabAt(browser(), GURL("about:blank"), 0, true);
// Calculate default location offset from work area.
gfx::Point top_right =
display::Screen::Get()->GetPrimaryDisplay().work_area().top_right();
int expected_x = top_right.x() - GlicWidget::GetInitialSize().width() -
glic::kDefaultDetachedTopRightDistance;
int expected_y = top_right.y() + glic::kDefaultDetachedTopRightDistance;
gfx::Point default_origin(expected_x, expected_y);
// Check that with no saved position the default location is used.
gfx::Rect initial_bounds = window_controller().GetInitialBounds(nullptr);
EXPECT_EQ(initial_bounds.origin(), default_origin);
// Initial bounds with browser are valid and not default location.
initial_bounds = window_controller().GetInitialBounds(browser());
EXPECT_NE(initial_bounds.origin(), default_origin);
// Use default location if Glic button location results in an invalid widget
// location. Move browser window so that it is mostly off the screen to the
// right.
browser()->window()->SetBounds(
{{top_right.x() + 500, top_right.y() + 50}, {900, 900}});
initial_bounds = window_controller().GetInitialBounds(browser());
EXPECT_EQ(initial_bounds.origin(), default_origin);
gfx::Rect screen_bounds =
display::Screen::Get()->GetPrimaryDisplay().bounds();
struct TestPair {
gfx::Point test;
gfx::Point expected;
std::string msg;
};
std::vector<TestPair> test_points = {
{{10, 20}, {10, 20}, "Valid position on screen"},
// Valid positions off each corner.
{{-20, -2}, {-20, -2}, "Valid top-left"},
{{-20, screen_bounds.height() - 100},
{-20, screen_bounds.height() - 100},
"Valid bottom left"},
{{screen_bounds.width() - initial_bounds.width() + 20,
screen_bounds.height() - 100},
{screen_bounds.width() - initial_bounds.width() + 20,
screen_bounds.height() - 100},
"Valid bottom right"},
{{screen_bounds.width() - initial_bounds.width() + 20, -2},
{screen_bounds.width() - initial_bounds.width() + 20, -2},
"Valid top right"},
// Invalid positions off of each edge
{{10, -5}, default_origin, "Invalid top"},
{{-400, 10}, default_origin, "Invalid left"},
{{10, screen_bounds.height() + 600}, default_origin, "Invalid bottom"},
{{screen_bounds.width() + 400, 10}, default_origin, "Invalid right"},
};
for (auto& t : test_points) {
window_controller().SetPreviousPositionForTesting(t.test);
initial_bounds = window_controller().GetInitialBounds(nullptr);
EXPECT_EQ(initial_bounds.origin(), t.expected) << t.msg;
}
}
// TODO(b/426542319): Fix and enable tests on non-mac platforms.
#if BUILDFLAG(IS_MAC)
class GlicWindowControllerLocationMetricsUiTest
: public GlicWindowControllerUiTest {
public:
GlicWindowControllerLocationMetricsUiTest() {
features_.InitWithFeatures(
/*enabled_features=*/{},
/*disabled_features=*/{features::kGlicPanelResetOnSessionTimeout,
features::kGlicPanelResetSizeAndLocationOnOpen});
}
~GlicWindowControllerLocationMetricsUiTest() override = default;
private:
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(GlicWindowControllerLocationMetricsUiTest,
TestPositionMetrics) {
if (IsWorkAreaTooSmallForTest()) {
GTEST_SKIP()
<< "Test's work area bounds are too small for consistent results.";
}
// The GlicButton and Tabstrip are not actually shown until a tab is created.
chrome::AddTabAt(browser(), GURL("about:blank"), 0, true);
gfx::Rect work_area_bounds =
display::Screen::Get()->GetPrimaryDisplay().work_area();
// Work area is split into 9 cells.
gfx::Size cell_size = {work_area_bounds.width() / 3,
work_area_bounds.height() / 3};
// Set browser bounds to the center cell of the work area bounds.
gfx::Rect browser_bounds = gfx::Rect(
gfx::Point(work_area_bounds.width() / 3 + work_area_bounds.x(),
work_area_bounds.height() / 3 + work_area_bounds.y()),
cell_size);
browser()->window()->SetBounds(browser_bounds);
browser_bounds = browser()->window()->GetBounds();
base::HistogramTester tester;
auto open_and_close = [this,
&tester](ChromeRelativePosition expected_position) {
RunTestSequence(ActivateSurface(kBrowserViewElementId),
SimulateGlicHotkey(), WaitForAndInstrumentGlic(kNone),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
SimulateOsButton(), WaitForHide(test::kGlicHostElementId),
CheckControllerHasWidget(false));
tester.ExpectBucketCount("Glic.PositionOnChrome.OnOpen", expected_position,
1);
tester.ExpectBucketCount("Glic.PositionOnChrome.OnClose", expected_position,
1);
};
window_controller().SetPreviousPositionForTesting(work_area_bounds.origin());
open_and_close(ChromeRelativePosition::kAboveLeft);
window_controller().SetPreviousPositionForTesting(
{work_area_bounds.origin().x(), browser_bounds.origin().y()});
open_and_close(ChromeRelativePosition::kCenterLeft);
window_controller().SetPreviousPositionForTesting(
{work_area_bounds.origin().x(), browser_bounds.bottom()});
open_and_close(ChromeRelativePosition::kBelowLeft);
window_controller().SetPreviousPositionForTesting(
{browser_bounds.x(), work_area_bounds.origin().y()});
open_and_close(ChromeRelativePosition::kAboveCenter);
window_controller().SetPreviousPositionForTesting(browser_bounds.origin());
open_and_close(ChromeRelativePosition::kOverlap);
window_controller().SetPreviousPositionForTesting(
browser_bounds.bottom_left());
open_and_close(ChromeRelativePosition::kBelowCenter);
window_controller().SetPreviousPositionForTesting(
{browser_bounds.right(), work_area_bounds.y()});
open_and_close(ChromeRelativePosition::kAboveRight);
window_controller().SetPreviousPositionForTesting(browser_bounds.top_right());
open_and_close(ChromeRelativePosition::kCenterRight);
window_controller().SetPreviousPositionForTesting(
browser_bounds.bottom_right());
open_and_close(ChromeRelativePosition::kBelowRight);
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached));
browser()->window()->Minimize();
ASSERT_TRUE(ui_test_utils::WaitForMinimized(browser()));
EXPECT_FALSE(browser()->window()->IsActive());
RunTestSequence(CloseGlicWindow());
tester.ExpectBucketCount("Glic.PositionOnChrome.OnClose",
ChromeRelativePosition::kNoVisibleChromeBrowser, 1);
// ChromeRelativePosition::kChromeOnOtherDisplay isn't being tested since
// tests involving moving Glic to another display are flaky.
}
IN_PROC_BROWSER_TEST_F(GlicWindowControllerLocationMetricsUiTest,
TestPercentOverlapMetrics) {
if (IsWorkAreaTooSmallForTest()) {
GTEST_SKIP()
<< "Test's work area bounds are too small for consistent results.";
}
// The GlicButton and Tabstrip are not actually shown until a tab is created.
chrome::AddTabAt(browser(), GURL("about:blank"), 0, true);
// Set browser bounds to the center cell of the work area bounds.
gfx::Rect browser_bounds = gfx::Rect(50, 50, 500, 400);
browser()->window()->SetBounds(browser_bounds);
browser_bounds = browser()->window()->GetBounds();
gfx::Size glic_expected_size = GlicWidget::GetInitialSize();
base::HistogramTester tester;
auto open_and_close = [this,
&tester](PercentOverlap expected_percent_overlap) {
RunTestSequence(ActivateSurface(kBrowserViewElementId),
SimulateGlicHotkey(), WaitForAndInstrumentGlic(kNone),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
SimulateOsButton(), WaitForHide(test::kGlicHostElementId),
CheckControllerHasWidget(false));
tester.ExpectBucketCount("Glic.PercentOverlapWithBrowser.OnOpen",
expected_percent_overlap, 1);
tester.ExpectBucketCount("Glic.PercentOverlapWithBrowser.OnClose",
expected_percent_overlap, 1);
};
gfx::Point test_origin = browser_bounds.top_right();
window_controller().SetPreviousPositionForTesting(test_origin);
open_and_close(PercentOverlap::k0);
test_origin.Offset(-0.5 * glic_expected_size.width(), 0);
window_controller().SetPreviousPositionForTesting(test_origin);
open_and_close(PercentOverlap::k50);
test_origin = browser_bounds.origin();
window_controller().SetPreviousPositionForTesting(test_origin);
open_and_close(PercentOverlap::k100);
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached));
browser()->window()->Minimize();
ASSERT_TRUE(ui_test_utils::WaitForMinimized(browser()));
EXPECT_FALSE(browser()->window()->IsActive());
RunTestSequence(CloseGlicWindow());
tester.ExpectBucketCount("Glic.PositionOnChrome.OnClose",
ChromeRelativePosition::kNoVisibleChromeBrowser, 1);
}
#endif
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUiTest, PermanentlyDeleteProfile) {
ProfileManager* const profile_manager = g_browser_process->profile_manager();
Profile& profile1 = profiles::testing::CreateProfileSync(
profile_manager, profile_manager->GenerateNextProfileDirectoryPath());
Browser* const browser1 = CreateBrowser(&profile1);
GlicKeyedService* const service1 =
GlicKeyedServiceFactory::GetGlicKeyedService(browser1->profile());
service1->fre_controller().AcceptFre();
EXPECT_TRUE(service1->enabling().HasConsented());
// Open glic
g_browser_process->local_state()->SetBoolean(prefs::kGlicLauncherEnabled,
true);
service1->ToggleUI(nullptr, false, mojom::InvocationSource::kOsHotkey);
EXPECT_TRUE(service1->IsWindowShowing());
// Delete the second profile
profile_manager->GetDeleteProfileHelper().MaybeScheduleProfileForDeletion(
browser1->profile()->GetPath(), base::DoNothing(),
ProfileMetrics::DELETE_PROFILE_USER_MANAGER);
ui_test_utils::WaitForBrowserToClose(browser1);
EXPECT_FALSE(service1->IsWindowShowing());
}
class GlicWindowControllerWithPreviousPostionUiTest
: public GlicWindowControllerUiTest {
public:
GlicWindowControllerWithPreviousPostionUiTest() {
features_.InitWithFeatures(
/*enabled_features=*/{},
/*disabled_features=*/{features::kGlicPanelResetOnSessionTimeout,
features::kGlicPanelResetSizeAndLocationOnOpen,
features::kGlicPanelResetOnStart});
}
void SetUpBrowserContextKeyedServices(
content::BrowserContext* context) override {
// Set initial bounds via pref and check that they are used.
Profile::FromBrowserContext(context)->GetPrefs()->SetInteger(
prefs::kGlicPreviousPositionX, 20);
Profile::FromBrowserContext(context)->GetPrefs()->SetInteger(
prefs::kGlicPreviousPositionY, 10);
test::InteractiveGlicTest::SetUpBrowserContextKeyedServices(context);
}
private:
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(GlicWindowControllerWithPreviousPostionUiTest,
TestInitialBounds) {
// Check that the saved initial bounds are used.
gfx::Rect initial_bounds = window_controller().GetInitialBounds(nullptr);
ASSERT_EQ(initial_bounds.origin(), gfx::Point(20, 10));
}
class GlicWindowControllerUnloadOnCloseTest
: public GlicWindowControllerUiTest {
public:
GlicWindowControllerUnloadOnCloseTest() {
features_.InitAndEnableFeature(features::kGlicUnloadOnClose);
}
~GlicWindowControllerUnloadOnCloseTest() override = default;
auto CheckWebUiContentsExist(bool exist) {
return CheckResult(
[this]() { return !!GetHostForActiveTab()->webui_contents(); }, exist,
"CheckWebUiContentsExist");
}
private:
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(GlicWindowControllerUnloadOnCloseTest, UnloadOnClose) {
RunTestSequence(OpenGlicWindow(GlicWindowMode::kDetached),
CheckControllerHasWidget(true), CheckWebUiContentsExist(true),
CloseGlicWindow(), CheckWebUiContentsExist(false));
}
class GlicWindowControllerWithMemoryPressureUiTest
: public GlicWindowControllerUiTest {
public:
GlicWindowControllerWithMemoryPressureUiTest() {
features_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kGlicWarming,
{{features::kGlicWarmingDelayMs.name, "0"},
{features::kGlicWarmingJitterMs.name, "0"}}}},
/*disabled_features=*/{});
}
~GlicWindowControllerWithMemoryPressureUiTest() override = default;
void SetUp() override {
// This will temporarily disable preloading to ensure that we don't load the
// web client before we've initialized the embedded test server and can set
// the correct URL.
GlicProfileManager::ForceMemoryPressureForTesting(
base::MemoryPressureMonitor::MemoryPressureLevel::
MEMORY_PRESSURE_LEVEL_CRITICAL);
GlicWindowControllerUiTest::SetUp();
}
void TearDown() override {
GlicWindowControllerUiTest::TearDown();
GlicProfileManager::ForceMemoryPressureForTesting(std::nullopt);
}
protected:
auto ResetMemoryPressure() {
return Do([]() {
GlicProfileManager::ForceMemoryPressureForTesting(
base::MemoryPressureMonitor::MemoryPressureLevel::
MEMORY_PRESSURE_LEVEL_NONE);
});
}
auto TryPreload() {
return Do([this]() { glic_service()->TryPreload(); });
}
auto CheckWarmed() {
return Do([this]() { EXPECT_TRUE(window_controller().IsWarmed()); });
}
private:
base::test::ScopedFeatureList features_;
};
IN_PROC_BROWSER_TEST_F(GlicWindowControllerWithMemoryPressureUiTest, Preload) {
// TODO(crbug.com/411100559): Wait for preload completion rather than assuming
// that it will finish before the next step in the sequence.
RunTestSequence(
ResetMemoryPressure(), TryPreload(), CheckWarmed(),
PressButton(kGlicButtonElementId),
InAnyContext(
WaitForShow(kGlicViewElementId).SetMustRemainVisible(false)));
}
// These tests for dragging across multiple displays is for mac-only.
// On Windows11, this test times out in calling WaitForDisplaySizes() when
// setting up the virtual displays.
#if BUILDFLAG(IS_MAC)
class GlicWindowControllerMultipleDisplaysUiTest
: public GlicWindowControllerUiTest {
public:
GlicWindowControllerMultipleDisplaysUiTest() = default;
GlicWindowControllerMultipleDisplaysUiTest(
const GlicWindowControllerMultipleDisplaysUiTest&) = delete;
GlicWindowControllerMultipleDisplaysUiTest& operator=(
const GlicWindowControllerMultipleDisplaysUiTest&) = delete;
~GlicWindowControllerMultipleDisplaysUiTest() override = default;
// Create virtual displays as needed, ensuring 2 displays are available for
// testing multi-screen functionality.
bool SetUpVirtualDisplays() {
if (display::Screen::Get()->GetNumDisplays() > 1) {
return true;
}
if ((virtual_display_util_ = display::test::VirtualDisplayUtil::TryCreate(
display::Screen::Get()))) {
virtual_display_util_->AddDisplay(
display::test::VirtualDisplayUtil::k1024x768);
return true;
}
return false;
}
auto CheckDisplaysSetUp(bool is_set_up) {
return CheckResult([this]() { return SetPrimaryAndSecondaryDisplay(); },
is_set_up, "CheckDisplaysSetUp");
}
auto CheckWidgetMovedToSecondaryDisplay(bool expect_moved) {
return CheckResult(
[this]() {
return secondary_display_.id() ==
window_controller().GetGlicWidget()->GetNearestDisplay()->id();
},
expect_moved, "CheckWidgetMovedToSecondaryDisplay");
}
bool SetPrimaryAndSecondaryDisplay() {
display::Display primary_display =
display::Screen::Get()->GetPrimaryDisplay();
secondary_display_ =
ui_test_utils::GetSecondaryDisplay(display::Screen::Get());
return primary_display.id() && secondary_display_.id();
}
auto MoveWidgetToSecondDisplay() {
return Do([this]() {
const gfx::Point target(secondary_display_.bounds().CenterPoint());
// Replace with dragging simulation once supported.
window_controller().GetGlicWidget()->SetBounds(
gfx::Rect(target, window_controller()
.GetGlicWidget()
->GetWindowBoundsInScreen()
.size()));
});
}
auto DetachGlicWindow() {
return Do([this]() {
static_cast<GlicWindowControllerImpl&>(window_controller()).Detach();
});
}
void TearDownOnMainThread() override {
GlicWindowControllerUiTest::TearDownOnMainThread();
virtual_display_util_.reset();
}
private:
std::unique_ptr<display::test::VirtualDisplayUtil> virtual_display_util_;
display::Display secondary_display_;
};
// TODO(crbug.com/399703468): Flaky on Mac. Test is targeted for Mac only.
IN_PROC_BROWSER_TEST_F(GlicWindowControllerMultipleDisplaysUiTest,
DISABLED_MoveDetachedGlicWindowToSecondDisplay) {
if (!SetUpVirtualDisplays()) {
return;
}
RunTestSequence(CheckDisplaysSetUp(true),
OpenGlicWindow(GlicWindowMode::kDetached),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kDetached),
InAnyContext(MoveWidgetToSecondDisplay(),
CheckWidgetMovedToSecondaryDisplay(true)),
CloseGlicWindow(), CheckControllerHasWidget(false));
}
// TODO(crbug.com/399703468): Flaky on Mac. Test is targeted for Mac only.
IN_PROC_BROWSER_TEST_F(
GlicWindowControllerMultipleDisplaysUiTest,
DISABLED_DetachAttachedGlicWindowAndMoveToSecondDisplay) {
if (!SetUpVirtualDisplays()) {
return;
}
RunTestSequence(CheckDisplaysSetUp(true),
OpenGlicWindow(GlicWindowMode::kAttached),
CheckControllerHasWidget(true),
CheckControllerWidgetMode(GlicWindowMode::kAttached),
InAnyContext(DetachGlicWindow(), MoveWidgetToSecondDisplay(),
CheckWidgetMovedToSecondaryDisplay(true)));
}
#endif // BUILDFLAG(IS_MAC)
} // namespace glic