blob: eb6f99565f710aabe891aa1d10b4616315d58d33 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/corewm/tooltip_controller.h"
#include <memory>
#include <utility>
#include "base/at_exit.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/window_types.h"
#include "ui/aura/test/aura_test_base.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/font.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/render_text.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/buildflags.h"
#include "ui/views/corewm/test/tooltip_aura_test_api.h"
#include "ui/views/corewm/tooltip_aura.h"
#include "ui/views/corewm/tooltip_controller_test_helper.h"
#include "ui/views/corewm/tooltip_state_manager.h"
#include "ui/views/test/desktop_test_views_delegate.h"
#include "ui/views/test/native_widget_factory.h"
#include "ui/views/test/test_views_delegate.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/public/activation_client.h"
#include "ui/wm/public/tooltip_observer.h"
#if BUILDFLAG(IS_WIN)
#include "ui/base/win/scoped_ole_initializer.h"
#endif
#if BUILDFLAG(ENABLE_DESKTOP_AURA)
#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
#endif
namespace views::corewm::test {
namespace {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
class TestTooltipLacros : public Tooltip {
public:
TestTooltipLacros() = default;
TestTooltipLacros(const TestTooltipLacros&) = delete;
TestTooltipLacros& operator=(const TestTooltipLacros&) = delete;
~TestTooltipLacros() override {
tooltip_parent_ = nullptr;
state_manager_ = nullptr;
}
void AddObserver(wm::TooltipObserver* observer) override {}
void RemoveObserver(wm::TooltipObserver* observer) override {}
const std::u16string& tooltip_text() const { return tooltip_text_; }
// Tooltip:
int GetMaxWidth(const gfx::Point& location) const override { return 100; }
void Update(aura::Window* window,
const std::u16string& tooltip_text,
const gfx::Point& position,
const TooltipTrigger trigger) override {
tooltip_parent_ = window;
tooltip_text_ = tooltip_text;
anchor_point_ = position + window->GetBoundsInScreen().OffsetFromOrigin();
trigger_ = trigger;
}
void Show() override {
is_visible_ = true;
DCHECK(state_manager_);
state_manager_->OnTooltipShownOnServer(tooltip_parent_, tooltip_text_,
gfx::Rect());
}
void Hide() override {
is_visible_ = false;
DCHECK(state_manager_);
state_manager_->OnTooltipHiddenOnServer();
}
bool IsVisible() override { return is_visible_; }
void SetStateManager(TooltipStateManager* state_manager) {
state_manager_ = state_manager;
}
const gfx::Point& anchor_point() { return anchor_point_; }
TooltipTrigger trigger() { return trigger_; }
private:
bool is_visible_ = false;
raw_ptr<aura::Window> tooltip_parent_ = nullptr;
raw_ptr<TooltipStateManager> state_manager_ = nullptr; // not owned.
std::u16string tooltip_text_;
gfx::Point anchor_point_;
TooltipTrigger trigger_;
};
#endif
views::Widget* CreateWidget(aura::Window* root) {
views::Widget* widget = new views::Widget;
views::Widget::InitParams params;
params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
params.accept_events = true;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
#if !BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_WIN)
params.parent = root;
#endif
params.bounds = gfx::Rect(0, 0, 200, 100);
widget->Init(std::move(params));
widget->Show();
return widget;
}
} // namespace
class TooltipControllerTest : public ViewsTestBase {
public:
TooltipControllerTest() = default;
TooltipControllerTest(const TooltipControllerTest&) = delete;
TooltipControllerTest& operator=(const TooltipControllerTest&) = delete;
~TooltipControllerTest() override = default;
void SetUp() override {
#if BUILDFLAG(ENABLE_DESKTOP_AURA)
set_native_widget_type(NativeWidgetType::kDesktop);
#endif
ViewsTestBase::SetUp();
aura::Window* root_window = GetContext();
#if !BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_WIN)
if (root_window) {
tooltip_ = new views::corewm::TooltipAura();
controller_ = std::make_unique<TooltipController>(
std::unique_ptr<views::corewm::Tooltip>(tooltip_),
/* activation_client */ nullptr);
root_window->AddPreTargetHandler(controller_.get());
SetTooltipClient(root_window, controller_.get());
}
#endif
widget_.reset(CreateWidget(root_window));
widget_->SetContentsView(std::make_unique<View>());
view_ = new TooltipTestView;
widget_->GetContentsView()->AddChildView(view_.get());
view_->SetBoundsRect(widget_->GetContentsView()->GetLocalBounds());
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Use TestTooltip instead of TooltipLacros to avoid using server side
// impl since it requires ui_controls which only works in
// interactive_ui_tests.
tooltip_ = new TestTooltipLacros();
controller_ = std::make_unique<TooltipController>(
std::unique_ptr<views::corewm::Tooltip>(tooltip_),
/*activation_client=*/nullptr);
widget_->GetNativeWindow()->GetRootWindow()->AddPreTargetHandler(
controller_.get());
// Set tooltip client after creating widget since tooltip controller is
// constructed and overriden inside CreateWidget() for Lacros.
SetTooltipClient(widget_->GetNativeWindow()->GetRootWindow(),
controller_.get());
#endif
helper_ = std::make_unique<TooltipControllerTestHelper>(
widget_->GetNativeWindow()->GetRootWindow());
#if BUILDFLAG(IS_CHROMEOS_LACROS)
tooltip_->SetStateManager(helper_->state_manager());
#endif
generator_ = std::make_unique<ui::test::EventGenerator>(GetRootWindow());
}
void TearDown() override {
#if !BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_WIN) || \
BUILDFLAG(IS_CHROMEOS_LACROS)
aura::Window* root_window = GetContext();
if (root_window) {
root_window->RemovePreTargetHandler(controller_.get());
wm::SetTooltipClient(root_window, nullptr);
controller_.reset();
}
#endif
generator_.reset();
helper_.reset();
widget_.reset();
ViewsTestBase::TearDown();
}
protected:
aura::Window* GetWindow() { return widget_->GetNativeWindow(); }
aura::Window* GetRootWindow() { return GetWindow()->GetRootWindow(); }
aura::Window* CreateNormalWindow(int id,
aura::Window* parent,
aura::WindowDelegate* delegate) {
aura::Window* window = new aura::Window(
delegate
? delegate
: aura::test::TestWindowDelegate::CreateSelfDestroyingDelegate());
window->SetId(id);
window->Init(ui::LAYER_TEXTURED);
parent->AddChild(window);
window->SetBounds(gfx::Rect(0, 0, 100, 100));
window->Show();
return window;
}
TooltipTestView* PrepareSecondView() {
TooltipTestView* view2 = new TooltipTestView;
widget_->GetContentsView()->AddChildView(view2);
view_->SetBounds(0, 0, 100, 100);
view2->SetBounds(100, 0, 100, 100);
return view2;
}
std::unique_ptr<views::Widget> widget_;
raw_ptr<TooltipTestView, DanglingUntriaged> view_ = nullptr;
std::unique_ptr<TooltipControllerTestHelper> helper_;
std::unique_ptr<ui::test::EventGenerator> generator_;
protected:
#if !BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_WIN)
raw_ptr<TooltipAura> tooltip_; // not owned.
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
raw_ptr<TestTooltipLacros> tooltip_; // not owned.
#endif
private:
std::unique_ptr<TooltipController> controller_;
#if BUILDFLAG(IS_WIN)
ui::ScopedOleInitializer ole_initializer_;
#endif
};
TEST_F(TooltipControllerTest, ViewTooltip) {
view_->set_tooltip_text(u"Tooltip Text");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseToCenterOf(GetWindow());
EXPECT_EQ(GetWindow(), GetRootWindow()->GetEventHandlerForPoint(
generator_->current_screen_location()));
std::u16string expected_tooltip = u"Tooltip Text";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
generator_->MoveMouseBy(1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
}
TEST_F(TooltipControllerTest, HideEmptyTooltip) {
view_->set_tooltip_text(u"Tooltip Text");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseToCenterOf(GetWindow());
generator_->MoveMouseBy(1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
view_->set_tooltip_text(u" ");
generator_->MoveMouseBy(1, 0);
EXPECT_FALSE(helper_->IsTooltipVisible());
}
TEST_F(TooltipControllerTest, DontShowTooltipOnTouch) {
view_->set_tooltip_text(u"Tooltip Text");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->PressMoveAndReleaseTouchToCenterOf(GetWindow());
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseToCenterOf(GetWindow());
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseBy(1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
std::u16string expected_tooltip = u"Tooltip Text";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
}
#if !BUILDFLAG(ENABLE_DESKTOP_AURA) || BUILDFLAG(IS_WIN)
// crbug.com/664370.
TEST_F(TooltipControllerTest, MaxWidth) {
std::u16string text =
u"Really, really, really, really, really, really long tooltip that "
u"exceeds max width";
view_->set_tooltip_text(text);
gfx::Point center = GetWindow()->bounds().CenterPoint();
generator_->MoveMouseTo(center);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
const gfx::RenderText* render_text =
test::TooltipAuraTestApi(tooltip_).GetRenderText();
int max = helper_->controller()->GetMaxWidth(center);
EXPECT_EQ(max, render_text->display_rect().width());
}
TEST_F(TooltipControllerTest, AccessibleNodeData) {
std::u16string text = u"Tooltip Text";
view_->set_tooltip_text(text);
gfx::Point center = GetWindow()->bounds().CenterPoint();
generator_->MoveMouseTo(center);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
ui::AXNodeData node_data;
test::TooltipAuraTestApi(tooltip_).GetAccessibleNodeData(&node_data);
EXPECT_EQ(ax::mojom::Role::kTooltip, node_data.role);
EXPECT_EQ(text, base::ASCIIToUTF16(node_data.GetStringAttribute(
ax::mojom::StringAttribute::kName)));
}
TEST_F(TooltipControllerTest, TooltipBounds) {
// We don't need a real tootip. Let's just use a custom size and custom point
// to test this function.
gfx::Size tooltip_size(100, 40);
gfx::Rect display_bounds(display::Screen::GetScreen()
->GetDisplayNearestPoint(gfx::Point(0, 0))
.bounds());
gfx::Point anchor_point = display_bounds.CenterPoint();
// All tests here share the same expected y value.
int a_expected_y(anchor_point.y() + TooltipAura::kCursorOffsetY);
int b_expected_y(anchor_point.y());
// 1. The tooltip fits entirely in the window.
{
// A. When attached to the cursor, the tooltip should be positioned at the
// bottom-right corner of the cursor.
gfx::Rect bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kCursor);
gfx::Point expected_position(anchor_point.x() + TooltipAura::kCursorOffsetX,
a_expected_y);
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
// B. When not attached to the cursor, the tooltip should be horizontally
// centered with the anchor point.
bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kKeyboard);
expected_position =
gfx::Point(anchor_point.x() - tooltip_size.width() / 2, b_expected_y);
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
}
// 2. The tooltip overflows on the left side of the window.
{
anchor_point = display_bounds.left_center();
anchor_point.Offset(-TooltipAura::kCursorOffsetX - 10, 0);
// A. When attached to the cursor, the tooltip should be positioned at the
// bottom-right corner of the cursor.
gfx::Rect bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kCursor);
gfx::Point expected_position(0, a_expected_y);
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
// B. When not attached to the cursor, the tooltip should be horizontally
// centered with the anchor point.
bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kKeyboard);
expected_position = gfx::Point(0, b_expected_y);
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
}
// 3. The tooltip overflows on the right side of the window.
{
anchor_point = display_bounds.right_center();
anchor_point.Offset(10, 0);
// A. When attached to the cursor, the tooltip should be positioned at the
// bottom-right corner of the cursor.
gfx::Rect bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kCursor);
gfx::Point expected_position(display_bounds.right() - tooltip_size.width(),
a_expected_y);
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
// B. When not attached to the cursor, the tooltip should be horizontally
// centered with the anchor point.
bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kKeyboard);
expected_position =
gfx::Point(display_bounds.right() - tooltip_size.width(), b_expected_y);
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
}
// 4. The tooltip overflows on the bottom.
{
anchor_point = display_bounds.bottom_center();
// A. When attached to the cursor, the tooltip should be positioned at the
// bottom-right corner of the cursor.
gfx::Rect bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kCursor);
gfx::Point expected_position(anchor_point.x() + TooltipAura::kCursorOffsetX,
anchor_point.y() - tooltip_size.height());
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
// B. When not attached to the cursor, the tooltip should be horizontally
// centered with the anchor point.
bounds = test::TooltipAuraTestApi(tooltip_).GetTooltipBounds(
tooltip_size, anchor_point, TooltipTrigger::kKeyboard);
expected_position = gfx::Point(anchor_point.x() - tooltip_size.width() / 2,
anchor_point.y() - tooltip_size.height());
EXPECT_EQ(bounds, gfx::Rect(expected_position, tooltip_size));
}
}
#endif
TEST_F(TooltipControllerTest, TooltipsInMultipleViews) {
view_->set_tooltip_text(u"Tooltip Text");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
PrepareSecondView();
aura::Window* window = GetWindow();
aura::Window* root_window = GetRootWindow();
generator_->MoveMouseRelativeTo(window, view_->bounds().CenterPoint());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
for (int i = 0; i < 49; ++i) {
generator_->MoveMouseBy(1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(window, root_window->GetEventHandlerForPoint(
generator_->current_screen_location()));
std::u16string expected_tooltip = u"Tooltip Text";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetTooltipParentWindow());
}
for (int i = 0; i < 49; ++i) {
generator_->MoveMouseBy(1, 0);
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_EQ(window, root_window->GetEventHandlerForPoint(
generator_->current_screen_location()));
std::u16string expected_tooltip; // = ""
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetTooltipParentWindow());
}
}
TEST_F(TooltipControllerTest, EnableOrDisableTooltips) {
view_->set_tooltip_text(u"Tooltip Text");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseRelativeTo(GetWindow(), view_->bounds().CenterPoint());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
// Disable tooltips and check again.
helper_->controller()->SetTooltipsEnabled(false);
EXPECT_FALSE(helper_->IsTooltipVisible());
helper_->UpdateIfRequired(TooltipTrigger::kCursor);
EXPECT_FALSE(helper_->IsTooltipVisible());
// Enable tooltips back and check again.
helper_->controller()->SetTooltipsEnabled(true);
EXPECT_FALSE(helper_->IsTooltipVisible());
helper_->UpdateIfRequired(TooltipTrigger::kCursor);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
}
// Verifies tooltip isn't shown if tooltip text consists entirely of whitespace.
TEST_F(TooltipControllerTest, DontShowEmptyTooltips) {
view_->set_tooltip_text(u" ");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseRelativeTo(GetWindow(), view_->bounds().CenterPoint());
EXPECT_FALSE(helper_->IsTooltipVisible());
}
// Disabled on Lacros since TooltipLacros does not have tooltip timer on client
// side so cannot be tested on unittest.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_TooltipUpdateWhenTooltipDeferTimerIsRunning \
DISABLED_TooltipUpdateWhenTooltipDeferTimerIsRunning
#else
#define MAYBE_TooltipUpdateWhenTooltipDeferTimerIsRunning \
TooltipUpdateWhenTooltipDeferTimerIsRunning
#endif
TEST_F(TooltipControllerTest,
MAYBE_TooltipUpdateWhenTooltipDeferTimerIsRunning) {
view_->set_tooltip_text(u"Tooltip Text for view 1");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
TooltipTestView* view2 = PrepareSecondView();
view2->set_tooltip_text(u"Tooltip Text for view 2");
aura::Window* window = GetWindow();
// Tooltips show up with delay
helper_->SkipTooltipShowDelay(false);
// Tooltip 1 is scheduled and invisibled
generator_->MoveMouseRelativeTo(window, view_->bounds().CenterPoint());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_FALSE(helper_->IsWillHideTooltipTimerRunning());
// Tooltip 2 is scheduled and invisible, the expected tooltip is tooltip 2
generator_->MoveMouseRelativeTo(window, view2->bounds().CenterPoint());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_FALSE(helper_->IsWillHideTooltipTimerRunning());
std::u16string expected_tooltip = u"Tooltip Text for view 2";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetTooltipParentWindow());
helper_->SkipTooltipShowDelay(true);
}
TEST_F(TooltipControllerTest, TooltipHidesOnKeyPressAndStaysHiddenUntilChange) {
view_->set_tooltip_text(u"Tooltip Text for view 1");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
TooltipTestView* view2 = PrepareSecondView();
view2->set_tooltip_text(u"Tooltip Text for view 2");
aura::Window* window = GetWindow();
generator_->MoveMouseRelativeTo(window, view_->bounds().CenterPoint());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_TRUE(helper_->IsWillHideTooltipTimerRunning());
#endif
generator_->PressKey(ui::VKEY_1, 0);
EXPECT_FALSE(helper_->IsTooltipVisible());
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_FALSE(helper_->IsWillHideTooltipTimerRunning());
#endif
// Moving the mouse inside |view1| should not change the state of the tooltip
// or the timers.
for (int i = 0; i < 49; i++) {
generator_->MoveMouseBy(1, 0);
EXPECT_FALSE(helper_->IsTooltipVisible());
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_FALSE(helper_->IsWillHideTooltipTimerRunning());
#endif
EXPECT_EQ(window, GetRootWindow()->GetEventHandlerForPoint(
generator_->current_screen_location()));
std::u16string expected_tooltip = u"Tooltip Text for view 1";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetObservedWindow());
}
// Now we move the mouse on to |view2|. It should update the tooltip.
generator_->MoveMouseBy(1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_TRUE(helper_->IsWillHideTooltipTimerRunning());
#endif
std::u16string expected_tooltip = u"Tooltip Text for view 2";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetTooltipParentWindow());
}
TEST_F(TooltipControllerTest, TooltipStaysVisibleOnKeyRelease) {
view_->set_tooltip_text(u"my tooltip");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
generator_->MoveMouseRelativeTo(GetWindow(), view_->bounds().CenterPoint());
EXPECT_TRUE(helper_->IsTooltipVisible());
// This shouldn't hide the tooltip.
generator_->ReleaseKey(ui::VKEY_1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
// This should hide the tooltip.
generator_->PressKey(ui::VKEY_1, 0);
EXPECT_FALSE(helper_->IsTooltipVisible());
}
TEST_F(TooltipControllerTest, TooltipHidesOnTimeoutAndStaysHiddenUntilChange) {
view_->set_tooltip_text(u"Tooltip Text for view 1");
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
TooltipTestView* view2 = PrepareSecondView();
view2->set_tooltip_text(u"Tooltip Text for view 2");
aura::Window* window = GetWindow();
// Update tooltip so tooltip becomes visible.
generator_->MoveMouseRelativeTo(window, view_->bounds().CenterPoint());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_TRUE(helper_->IsWillHideTooltipTimerRunning());
#endif
helper_->FireHideTooltipTimer();
EXPECT_FALSE(helper_->IsTooltipVisible());
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_FALSE(helper_->IsWillHideTooltipTimerRunning());
#endif
// Moving the mouse inside |view1| should not change the state of the tooltip
// or the timers.
for (int i = 0; i < 49; ++i) {
generator_->MoveMouseBy(1, 0);
EXPECT_FALSE(helper_->IsTooltipVisible());
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_FALSE(helper_->IsWillHideTooltipTimerRunning());
#endif
EXPECT_EQ(window, GetRootWindow()->GetEventHandlerForPoint(
generator_->current_screen_location()));
std::u16string expected_tooltip = u"Tooltip Text for view 1";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetObservedWindow());
}
// Now we move the mouse on to |view2|. It should update the tooltip.
generator_->MoveMouseBy(1, 0);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
EXPECT_TRUE(helper_->IsWillHideTooltipTimerRunning());
#endif
std::u16string expected_tooltip = u"Tooltip Text for view 2";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(window));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(window, helper_->GetTooltipParentWindow());
}
// Verifies a mouse exit event hides the tooltips.
TEST_F(TooltipControllerTest, HideOnExit) {
view_->set_tooltip_text(u"Tooltip Text");
generator_->MoveMouseToCenterOf(GetWindow());
std::u16string expected_tooltip = u"Tooltip Text";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
generator_->SendMouseExit();
EXPECT_FALSE(helper_->IsTooltipVisible());
}
TEST_F(TooltipControllerTest, ReshowOnClickAfterEnterExit) {
// Owned by |view_|.
TooltipTestView* v1 = new TooltipTestView;
TooltipTestView* v2 = new TooltipTestView;
view_->AddChildView(v1);
view_->AddChildView(v2);
gfx::Rect view_bounds(view_->GetLocalBounds());
view_bounds.set_height(view_bounds.height() / 2);
v1->SetBoundsRect(view_bounds);
view_bounds.set_y(view_bounds.height());
v2->SetBoundsRect(view_bounds);
const std::u16string v1_tt(u"v1");
const std::u16string v2_tt(u"v2");
v1->set_tooltip_text(v1_tt);
v2->set_tooltip_text(v2_tt);
gfx::Point v1_point(1, 1);
View::ConvertPointToWidget(v1, &v1_point);
generator_->MoveMouseRelativeTo(GetWindow(), v1_point);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(v1_tt, helper_->GetTooltipText());
// Press the mouse, move to v2 and back to v1.
generator_->ClickLeftButton();
gfx::Point v2_point(1, 1);
View::ConvertPointToWidget(v2, &v2_point);
generator_->MoveMouseRelativeTo(GetWindow(), v2_point);
generator_->MoveMouseRelativeTo(GetWindow(), v1_point);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(v1_tt, helper_->GetTooltipText());
}
TEST_F(TooltipControllerTest, ShowAndHideTooltipTriggeredFromKeyboard) {
std::u16string expected_tooltip = u"Tooltip Text";
wm::SetTooltipText(GetWindow(), &expected_tooltip);
view_->set_tooltip_text(expected_tooltip);
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
helper_->controller()->UpdateTooltipFromKeyboard(
view_->ConvertRectToWidget(view_->bounds()), GetWindow());
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kKeyboard);
helper_->HideAndReset();
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
}
TEST_F(TooltipControllerTest,
KeyboardTriggeredTooltipStaysVisibleOnMouseExitedEvent) {
std::u16string expected_tooltip = u"Tooltip Text";
wm::SetTooltipText(GetWindow(), &expected_tooltip);
view_->set_tooltip_text(expected_tooltip);
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
// For this test to execute properly, make sure that the cursor location is
// somewhere out of the |view_|, different than (0, 0). This shouldn't show
// the tooltip.
gfx::Point off_view_point = view_->bounds().bottom_right();
off_view_point.Offset(1, 1);
generator_->MoveMouseRelativeTo(widget_->GetNativeWindow(), off_view_point);
EXPECT_FALSE(helper_->IsTooltipVisible());
// Trigger the tooltip from the keyboard.
helper_->controller()->UpdateTooltipFromKeyboard(
view_->ConvertRectToWidget(view_->bounds()), GetWindow());
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kKeyboard);
// Sending a mouse exited event shouldn't hide a keyboard triggered tooltip.
generator_->SendMouseExit();
EXPECT_TRUE(helper_->IsTooltipVisible());
helper_->HideAndReset();
expected_tooltip = u"Tooltip Text 2";
EXPECT_FALSE(helper_->IsTooltipVisible());
// However, a cursor triggered tooltip should still be hidden by a mouse
// exited event.
generator_->MoveMouseRelativeTo(widget_->GetNativeWindow(),
view_->bounds().CenterPoint());
EXPECT_TRUE(helper_->IsTooltipVisible());
generator_->SendMouseExit();
EXPECT_FALSE(helper_->IsTooltipVisible());
}
namespace {
// Returns the index of |window| in its parent's children.
int IndexInParent(const aura::Window* window) {
auto i = base::ranges::find(window->parent()->children(), window);
return i == window->parent()->children().end()
? -1
: static_cast<int>(i - window->parent()->children().begin());
}
} // namespace
// Verifies when capture is released the TooltipController resets state.
// Flaky on all builders. http://crbug.com/388268
TEST_F(TooltipControllerTest, DISABLED_CloseOnCaptureLost) {
view_->GetWidget()->SetCapture(view_);
RunPendingMessages();
view_->set_tooltip_text(u"Tooltip Text");
generator_->MoveMouseToCenterOf(GetWindow());
std::u16string expected_tooltip = u"Tooltip Text";
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
view_->GetWidget()->ReleaseCapture();
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_TRUE(helper_->GetTooltipParentWindow() == nullptr);
}
// Disabled on Linux as X11ScreenOzone::GetAcceleratedWidgetAtScreenPoint
// and WaylandScreen::GetAcceleratedWidgetAtScreenPoint don't consider z-order.
// Disabled on Windows due to failing bots. http://crbug.com/604479
// Disabled on Lacros due to crash and flakiness.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_Capture DISABLED_Capture
#else
#define MAYBE_Capture Capture
#endif
// Verifies the correct window is found for tooltips when there is a capture.
TEST_F(TooltipControllerTest, MAYBE_Capture) {
const std::u16string tooltip_text(u"1");
const std::u16string tooltip_text2(u"2");
const std::u16string tooltip_text_child(u"child");
widget_->SetBounds(gfx::Rect(0, 0, 200, 200));
view_->set_tooltip_text(tooltip_text);
std::unique_ptr<views::Widget> widget2(CreateWidget(GetContext()));
widget2->SetContentsView(std::make_unique<View>());
TooltipTestView* view2 = new TooltipTestView;
widget2->GetContentsView()->AddChildView(view2);
view2->set_tooltip_text(tooltip_text2);
widget2->SetBounds(gfx::Rect(0, 0, 200, 200));
view2->SetBoundsRect(widget2->GetContentsView()->GetLocalBounds());
widget_->SetCapture(view_);
EXPECT_TRUE(widget_->HasCapture());
widget2->Show();
EXPECT_GE(IndexInParent(widget2->GetNativeWindow()),
IndexInParent(widget_->GetNativeWindow()));
generator_->MoveMouseRelativeTo(widget_->GetNativeWindow(),
view_->bounds().CenterPoint());
// Even though the mouse is over a window with a tooltip it shouldn't be
// picked up because the windows don't have the same value for
// |TooltipManager::kGroupingPropertyKey|.
EXPECT_TRUE(helper_->GetTooltipText().empty());
// Now make both the windows have same transient value for
// kGroupingPropertyKey. In this case the tooltip should be picked up from
// |widget2| (because the mouse is over it).
const int grouping_key = 1;
widget_->SetNativeWindowProperty(TooltipManager::kGroupingPropertyKey,
reinterpret_cast<void*>(grouping_key));
widget2->SetNativeWindowProperty(TooltipManager::kGroupingPropertyKey,
reinterpret_cast<void*>(grouping_key));
generator_->MoveMouseBy(1, 10);
EXPECT_EQ(tooltip_text2, helper_->GetTooltipText());
// Make child widget under widget2 and let the mouse be over the child widget.
// Even though the child widget does not have grouping property key, it should
// refer to its parent property. In this scenario, `widget_child`'s parent is
// `widget2` and it has the same kGroupingPropertyKey as `widget_`'s key, so
// `widget_child` should show tooltip when `widget_` has a capture.
std::unique_ptr<views::Widget> widget_child(CreateWidget(GetContext()));
widget_child->SetContentsView(std::make_unique<View>());
TooltipTestView* view_child = new TooltipTestView;
widget_child->GetContentsView()->AddChildView(view_child);
view_child->set_tooltip_text(tooltip_text_child);
widget_child->SetBounds(gfx::Rect(0, 0, 200, 200));
view_child->SetBoundsRect(widget_child->GetContentsView()->GetLocalBounds());
Widget::ReparentNativeView(widget_child->GetNativeView(),
widget2->GetNativeView());
widget_child->Show();
generator_->MoveMouseBy(1, 10);
EXPECT_EQ(tooltip_text_child, helper_->GetTooltipText());
widget2.reset();
}
TEST_F(TooltipControllerTest, ShowTooltipOnTooltipTextUpdate) {
std::u16string expected_tooltip;
wm::SetTooltipText(GetWindow(), &expected_tooltip);
// Create a mouse event. This event shouldn't trigger the tooltip to show
// since the tooltip text is empty, but should set the |observed_window_|
// correctly.
gfx::Point point(1, 1);
View::ConvertPointToWidget(view_, &point);
generator_->MoveMouseRelativeTo(GetWindow(), point);
EXPECT_EQ(std::u16string(), helper_->GetTooltipText());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
EXPECT_EQ(GetWindow(), helper_->GetObservedWindow());
EXPECT_FALSE(helper_->IsTooltipVisible());
// This is the heart of the test: we update the tooltip text and call
// UpdateTooltip. It should trigger the tooltip to show up because the
// |observed_window_| will be set to GetWindow() and the tooltip text on the
// window will be different than it previously was.
expected_tooltip = u"Tooltip text";
helper_->controller()->UpdateTooltip(GetWindow());
EXPECT_EQ(expected_tooltip, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_tooltip, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
helper_->HideAndReset();
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_EQ(nullptr, helper_->GetTooltipParentWindow());
}
// Disabled on Lacros since TooltipLacros does not have tooltip timer on client
// side so cannot be tested on unittest.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_TooltipPositionUpdatedWhenTimerRunning \
DISABLED_TooltipPositionUpdatedWhenTimerRunning
#else
#define MAYBE_TooltipPositionUpdatedWhenTimerRunning \
TooltipPositionUpdatedWhenTimerRunning
#endif
// This test validates that the TooltipController correctly triggers a position
// update for a tooltip that is about to be shown.
TEST_F(TooltipControllerTest, MAYBE_TooltipPositionUpdatedWhenTimerRunning) {
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(std::u16string(), helper_->state_manager()->tooltip_text());
std::u16string expected_text = u"Tooltip Text";
view_->set_tooltip_text(expected_text);
helper_->SkipTooltipShowDelay(false);
// Testing that the position will be updated when triggered from cursor.
{
gfx::Point position = view_->bounds().CenterPoint();
generator_->MoveMouseRelativeTo(GetWindow(), position);
EXPECT_EQ(expected_text, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_text, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(position, helper_->GetTooltipPosition());
// Since the |will_show_tooltip_timer_| is running, this should update the
// position of the already active tooltip.
generator_->MoveMouseBy(2, 0);
position.Offset(2, 0);
EXPECT_EQ(position, helper_->GetTooltipPosition());
helper_->HideAndReset();
}
// Testing that the position will be updated when triggered from cursor.
{
gfx::Rect bounds = view_->ConvertRectToWidget(view_->bounds());
helper_->controller()->UpdateTooltipFromKeyboard(bounds, GetWindow());
EXPECT_EQ(expected_text, wm::GetTooltipText(GetWindow()));
EXPECT_EQ(expected_text, helper_->GetTooltipText());
EXPECT_EQ(GetWindow(), helper_->GetTooltipParentWindow());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kKeyboard);
EXPECT_EQ(bounds.bottom_center(), helper_->GetTooltipPosition());
// Since the |will_show_tooltip_timer_| is running, this should update the
// position of the already active tooltip.
bounds.Offset(2, 0);
helper_->controller()->UpdateTooltipFromKeyboard(bounds, GetWindow());
EXPECT_EQ(bounds.bottom_center(), helper_->GetTooltipPosition());
helper_->HideAndReset();
}
helper_->SkipTooltipShowDelay(true);
}
// This test validates that tooltips are hidden when the currently active window
// loses focus to another window.
TEST_F(TooltipControllerTest, TooltipHiddenWhenWindowDeactivated) {
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(std::u16string(), helper_->state_manager()->tooltip_text());
view_->set_tooltip_text(u"Tooltip text 1");
// Start by showing the tooltip.
gfx::Point in_view_point = view_->bounds().CenterPoint();
generator_->MoveMouseRelativeTo(GetWindow(), in_view_point);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
// Then mock a window deactivation event.
helper_->MockWindowActivated(GetWindow(), /* active */ false);
// The previously visible tooltip should have been closed by that event.
EXPECT_FALSE(helper_->IsTooltipVisible());
// The tooltip should show up again if we move the cursor again.
view_->set_tooltip_text(u"Tooltip text 2");
generator_->MoveMouseBy(1, 1);
EXPECT_TRUE(helper_->IsTooltipVisible());
}
namespace {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
using TestTooltip = TestTooltipLacros;
#else
class TestTooltip : public Tooltip {
public:
TestTooltip() = default;
TestTooltip(const TestTooltip&) = delete;
TestTooltip& operator=(const TestTooltip&) = delete;
~TestTooltip() override = default;
void AddObserver(wm::TooltipObserver* observer) override {}
void RemoveObserver(wm::TooltipObserver* observer) override {}
const std::u16string& tooltip_text() const { return tooltip_text_; }
// Tooltip:
int GetMaxWidth(const gfx::Point& location) const override { return 100; }
void Update(aura::Window* window,
const std::u16string& tooltip_text,
const gfx::Point& position,
const TooltipTrigger trigger) override {
tooltip_text_ = tooltip_text;
anchor_point_ = position + window->GetBoundsInScreen().OffsetFromOrigin();
trigger_ = trigger;
}
void Show() override { is_visible_ = true; }
void Hide() override { is_visible_ = false; }
bool IsVisible() override { return is_visible_; }
const gfx::Point& anchor_point() { return anchor_point_; }
TooltipTrigger trigger() { return trigger_; }
private:
bool is_visible_ = false;
std::u16string tooltip_text_;
gfx::Point anchor_point_;
TooltipTrigger trigger_;
};
#endif
} // namespace
// Use for tests that don't depend upon views.
class TooltipControllerTest2 : public aura::test::AuraTestBase {
public:
TooltipControllerTest2() : test_tooltip_(new TestTooltip) {}
TooltipControllerTest2(const TooltipControllerTest2&) = delete;
TooltipControllerTest2& operator=(const TooltipControllerTest2&) = delete;
~TooltipControllerTest2() override = default;
void SetUp() override {
at_exit_manager_ = std::make_unique<base::ShadowingAtExitManager>();
aura::test::AuraTestBase::SetUp();
controller_ = std::make_unique<TooltipController>(
std::unique_ptr<corewm::Tooltip>(test_tooltip_),
/* activation_client */ nullptr);
root_window()->AddPreTargetHandler(controller_.get());
SetTooltipClient(root_window(), controller_.get());
helper_ = std::make_unique<TooltipControllerTestHelper>(root_window());
#if BUILDFLAG(IS_CHROMEOS_LACROS)
test_tooltip_->SetStateManager(helper_->state_manager());
#endif
generator_ = std::make_unique<ui::test::EventGenerator>(root_window());
}
void TearDown() override {
root_window()->RemovePreTargetHandler(controller_.get());
wm::SetTooltipClient(root_window(), nullptr);
controller_.reset();
generator_.reset();
helper_.reset();
aura::test::AuraTestBase::TearDown();
at_exit_manager_.reset();
}
protected:
// Owned by |controller_|.
raw_ptr<TestTooltip, DanglingUntriaged> test_tooltip_;
std::unique_ptr<TooltipControllerTestHelper> helper_;
std::unique_ptr<ui::test::EventGenerator> generator_;
private:
// Needed to make sure the DeviceDataManager is cleaned up between test runs.
std::unique_ptr<base::ShadowingAtExitManager> at_exit_manager_;
std::unique_ptr<TooltipController> controller_;
};
TEST_F(TooltipControllerTest2, VerifyLeadingTrailingWhitespaceStripped) {
aura::test::TestWindowDelegate test_delegate;
std::unique_ptr<aura::Window> window(
CreateNormalWindow(100, root_window(), &test_delegate));
window->SetBounds(gfx::Rect(0, 0, 300, 300));
std::u16string tooltip_text(u" \nx ");
wm::SetTooltipText(window.get(), &tooltip_text);
EXPECT_FALSE(helper_->IsTooltipVisible());
generator_->MoveMouseToCenterOf(window.get());
EXPECT_EQ(u"x", test_tooltip_->tooltip_text());
}
// Verifies that tooltip is hidden and tooltip window closed upon cancel mode.
TEST_F(TooltipControllerTest2, CloseOnCancelMode) {
aura::test::TestWindowDelegate test_delegate;
std::unique_ptr<aura::Window> window(
CreateNormalWindow(100, root_window(), &test_delegate));
window->SetBounds(gfx::Rect(0, 0, 300, 300));
std::u16string tooltip_text(u"Tooltip Text");
wm::SetTooltipText(window.get(), &tooltip_text);
EXPECT_FALSE(helper_->IsTooltipVisible());
generator_->MoveMouseToCenterOf(window.get());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
// Send OnCancelMode event and verify that tooltip becomes invisible and
// the tooltip window is closed.
ui::CancelModeEvent event;
helper_->controller()->OnCancelMode(&event);
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_TRUE(helper_->GetTooltipParentWindow() == nullptr);
}
// Use for tests that need both views and a TestTooltip.
class TooltipControllerTest3 : public ViewsTestBase {
public:
TooltipControllerTest3() = default;
TooltipControllerTest3(const TooltipControllerTest3&) = delete;
TooltipControllerTest3& operator=(const TooltipControllerTest3&) = delete;
~TooltipControllerTest3() override = default;
void SetUp() override {
#if BUILDFLAG(ENABLE_DESKTOP_AURA)
set_native_widget_type(NativeWidgetType::kDesktop);
#endif
ViewsTestBase::SetUp();
widget_.reset(CreateWidget(GetContext()));
widget_->SetContentsView(std::make_unique<View>());
view_ = new TooltipTestView;
widget_->GetContentsView()->AddChildView(view_.get());
view_->SetBoundsRect(widget_->GetContentsView()->GetLocalBounds());
generator_ = std::make_unique<ui::test::EventGenerator>(GetRootWindow());
auto tooltip = std::make_unique<TestTooltip>();
test_tooltip_ = tooltip.get();
controller_ = std::make_unique<TooltipController>(
std::move(tooltip), /* activation_client */ nullptr);
auto* tooltip_controller =
static_cast<TooltipController*>(wm::GetTooltipClient(GetRootWindow()));
if (tooltip_controller)
GetRootWindow()->RemovePreTargetHandler(tooltip_controller);
GetRootWindow()->AddPreTargetHandler(controller_.get());
SetTooltipClient(GetRootWindow(), controller_.get());
helper_ = std::make_unique<TooltipControllerTestHelper>(GetRootWindow());
#if BUILDFLAG(IS_CHROMEOS_LACROS)
test_tooltip_->SetStateManager(helper_->state_manager());
#endif
}
void TearDown() override {
GetRootWindow()->RemovePreTargetHandler(controller_.get());
wm::SetTooltipClient(GetRootWindow(), nullptr);
controller_.reset();
generator_.reset();
helper_.reset();
widget_.reset();
ViewsTestBase::TearDown();
}
aura::Window* GetWindow() { return widget_->GetNativeWindow(); }
protected:
// Owned by |controller_|.
raw_ptr<TestTooltip, DanglingUntriaged> test_tooltip_ = nullptr;
std::unique_ptr<TooltipControllerTestHelper> helper_;
std::unique_ptr<ui::test::EventGenerator> generator_;
std::unique_ptr<views::Widget> widget_;
raw_ptr<TooltipTestView, DanglingUntriaged> view_;
private:
std::unique_ptr<TooltipController> controller_;
#if BUILDFLAG(IS_WIN)
ui::ScopedOleInitializer ole_initializer_;
#endif
aura::Window* GetRootWindow() { return GetWindow()->GetRootWindow(); }
};
TEST_F(TooltipControllerTest3, TooltipPositionChangesOnTwoViewsWithSameLabel) {
// Owned by |view_|.
// These two views have the same tooltip text
TooltipTestView* v1 = new TooltipTestView;
TooltipTestView* v2 = new TooltipTestView;
// v1_1 is a view inside v1 that has an identical tooltip text to that of v1
// and v2
TooltipTestView* v1_1 = new TooltipTestView;
// v2_1 is a view inside v2 that has an identical tooltip text to that of v1
// and v2
TooltipTestView* v2_1 = new TooltipTestView;
// v2_2 is a view inside v2 with the tooltip text different from all the
// others
TooltipTestView* v2_2 = new TooltipTestView;
// Setup all the views' relations
view_->AddChildView(v1);
view_->AddChildView(v2);
v1->AddChildView(v1_1);
v2->AddChildView(v2_1);
v2->AddChildView(v2_2);
const std::u16string reference_string(u"Identical Tooltip Text");
const std::u16string alternative_string(u"Another Shrubbery");
v1->set_tooltip_text(reference_string);
v2->set_tooltip_text(reference_string);
v1_1->set_tooltip_text(reference_string);
v2_1->set_tooltip_text(reference_string);
v2_2->set_tooltip_text(alternative_string);
// Set views' bounds
gfx::Rect view_bounds(view_->GetLocalBounds());
view_bounds.set_height(view_bounds.height() / 2);
v1->SetBoundsRect(view_bounds);
v1_1->SetBounds(0, 0, 3, 3);
view_bounds.set_y(view_bounds.height());
v2->SetBoundsRect(view_bounds);
v2_2->SetBounds(view_bounds.width() - 3, view_bounds.height() - 3, 3, 3);
v2_1->SetBounds(0, 0, 3, 3);
// Test whether a toolbar appears on v1
gfx::Point center = v1->bounds().CenterPoint();
generator_->MoveMouseRelativeTo(GetWindow(), center);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(reference_string, helper_->GetTooltipText());
gfx::Point tooltip_bounds1 = test_tooltip_->anchor_point();
// Test whether the toolbar changes position on mouse over v2
center = v2->bounds().CenterPoint();
generator_->MoveMouseRelativeTo(GetWindow(), center);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(reference_string, helper_->GetTooltipText());
gfx::Point tooltip_bounds2 = test_tooltip_->anchor_point();
EXPECT_NE(tooltip_bounds1, gfx::Point());
EXPECT_NE(tooltip_bounds2, gfx::Point());
EXPECT_NE(tooltip_bounds1, tooltip_bounds2);
// Test if the toolbar does not change position on encountering a contained
// view with the same tooltip text
center = v2_1->GetLocalBounds().CenterPoint();
views::View::ConvertPointToTarget(v2_1, view_, &center);
generator_->MoveMouseRelativeTo(GetWindow(), center);
gfx::Point tooltip_bounds2_1 = test_tooltip_->anchor_point();
EXPECT_NE(tooltip_bounds2, tooltip_bounds2_1);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(reference_string, helper_->GetTooltipText());
// Test if the toolbar changes position on encountering a contained
// view with a different tooltip text
center = v2_2->GetLocalBounds().CenterPoint();
views::View::ConvertPointToTarget(v2_2, view_, &center);
generator_->MoveMouseRelativeTo(GetWindow(), center);
gfx::Point tooltip_bounds2_2 = test_tooltip_->anchor_point();
EXPECT_NE(tooltip_bounds2_1, tooltip_bounds2_2);
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(alternative_string, helper_->GetTooltipText());
// Test if moving from a view that is contained by a larger view, both with
// the same tooltip text, does not change tooltip's position.
center = v1_1->GetLocalBounds().CenterPoint();
views::View::ConvertPointToTarget(v1_1, view_, &center);
generator_->MoveMouseRelativeTo(GetWindow(), center);
gfx::Point tooltip_bounds1_1 = test_tooltip_->anchor_point();
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
EXPECT_EQ(reference_string, helper_->GetTooltipText());
center = v1->bounds().CenterPoint();
generator_->MoveMouseRelativeTo(GetWindow(), center);
tooltip_bounds1 = test_tooltip_->anchor_point();
EXPECT_NE(tooltip_bounds1_1, tooltip_bounds1);
EXPECT_EQ(reference_string, helper_->GetTooltipText());
}
class TooltipStateManagerTest : public TooltipControllerTest {
public:
TooltipStateManagerTest() = default;
TooltipStateManagerTest(const TooltipStateManagerTest&) = delete;
TooltipStateManagerTest& operator=(const TooltipStateManagerTest&) = delete;
~TooltipStateManagerTest() override = default;
};
TEST_F(TooltipStateManagerTest, ShowAndHideTooltip) {
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(std::u16string(), helper_->state_manager()->tooltip_text());
std::u16string expected_text = u"Tooltip Text";
helper_->state_manager()->Show(GetRootWindow(), expected_text,
gfx::Point(0, 0), TooltipTrigger::kCursor,
helper_->GetShowTooltipDelay(), {});
EXPECT_EQ(GetRootWindow(), helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(expected_text, helper_->state_manager()->tooltip_text());
EXPECT_TRUE(helper_->IsTooltipVisible());
EXPECT_EQ(helper_->state_manager()->tooltip_trigger(),
TooltipTrigger::kCursor);
helper_->HideAndReset();
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
// We don't clear the text of the next tooltip because we use to validate that
// we're not about to show a tooltip that has been explicitly hidden.
// TODO(bebeaudr): Update this when we have a truly unique tooltipd id, even
// for web content.
EXPECT_EQ(expected_text, helper_->state_manager()->tooltip_text());
EXPECT_FALSE(helper_->IsTooltipVisible());
}
// Disabled on Lacros since TooltipLacros cannot handle tooltip with delay on
// client side properly. To test with delay, it needs to use Ash server with
// ui_controls in interactive_ui_tests.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_ShowTooltipWithDelay DISABLED_ShowTooltipWithDelay
#else
#define MAYBE_ShowTooltipWithDelay ShowTooltipWithDelay
#endif
TEST_F(TooltipStateManagerTest, MAYBE_ShowTooltipWithDelay) {
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(std::u16string(), helper_->state_manager()->tooltip_text());
std::u16string expected_text = u"Tooltip Text";
helper_->SkipTooltipShowDelay(false);
// 1. Showing the tooltip will start the |will_show_tooltip_timer_| and set
// the attributes, but won't make the tooltip visible.
helper_->state_manager()->Show(GetRootWindow(), expected_text,
gfx::Point(0, 0), TooltipTrigger::kCursor,
helper_->GetShowTooltipDelay(), {});
EXPECT_EQ(GetRootWindow(), helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(expected_text, helper_->state_manager()->tooltip_text());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_TRUE(helper_->IsWillShowTooltipTimerRunning());
// 2. Showing the tooltip again with a different expected text will cancel the
// existing timers running and will update the text, but it still won't make
// the tooltip visible.
expected_text = u"Tooltip Text 2";
helper_->state_manager()->Show(GetRootWindow(), expected_text,
gfx::Point(0, 0), TooltipTrigger::kCursor,
helper_->GetShowTooltipDelay(), {});
EXPECT_EQ(GetRootWindow(), helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(expected_text, helper_->state_manager()->tooltip_text());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_TRUE(helper_->IsWillShowTooltipTimerRunning());
// 3. Calling HideAndReset should cancel the timer running.
helper_->HideAndReset();
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_FALSE(helper_->IsWillShowTooltipTimerRunning());
helper_->SkipTooltipShowDelay(true);
}
// Disabled on Lacros since TooltipLacros cannot handle tooltip with delay on
// client side properly. To test with delay, it needs to use Ash server with
// ui_controls in interactive_ui_tests.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_UpdatePositionIfNeeded DISABLED_UpdatePositionIfNeeded
#else
#define MAYBE_UpdatePositionIfNeeded UpdatePositionIfNeeded
#endif
// This test ensures that we can update the position of the tooltip after the
// |will_show_tooltip_timer_| has been started. This is needed because the
// cursor might still move between the moment Show is called and the timer
// fires.
TEST_F(TooltipStateManagerTest, MAYBE_UpdatePositionIfNeeded) {
EXPECT_EQ(nullptr, helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(std::u16string(), helper_->state_manager()->tooltip_text());
std::u16string expected_text = u"Tooltip Text";
helper_->SkipTooltipShowDelay(false);
{
gfx::Point position(0, 0);
// 1. When the |will_show_tooltip_timer_| is running, validate that we can
// update the position.
helper_->state_manager()->Show(GetRootWindow(), expected_text, position,
TooltipTrigger::kCursor,
helper_->GetShowTooltipDelay(), {});
EXPECT_EQ(GetRootWindow(),
helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(expected_text, helper_->state_manager()->tooltip_text());
EXPECT_EQ(position, helper_->GetTooltipPosition());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_TRUE(helper_->IsWillShowTooltipTimerRunning());
gfx::Point new_position = gfx::Point(10, 10);
// Because the tooltip was triggered by the cursor, the position should be
// updated by a keyboard triggered modification.
helper_->state_manager()->UpdatePositionIfNeeded(new_position,
TooltipTrigger::kKeyboard);
EXPECT_EQ(position, helper_->GetTooltipPosition());
// But it should be updated when the position's update is triggered by the
// cursor.
helper_->state_manager()->UpdatePositionIfNeeded(new_position,
TooltipTrigger::kCursor);
EXPECT_EQ(new_position, helper_->GetTooltipPosition());
// 2. Validate that we can't update the position when the timer isn't
// running.
helper_->HideAndReset();
position = new_position;
new_position = gfx::Point(20, 20);
helper_->state_manager()->UpdatePositionIfNeeded(new_position,
TooltipTrigger::kCursor);
EXPECT_EQ(position, helper_->GetTooltipPosition());
}
{
gfx::Point position(0, 0);
// 1. When the |will_show_tooltip_timer_| is running, validate that we can
// update the position.
helper_->state_manager()->Show(GetRootWindow(), expected_text, position,
TooltipTrigger::kKeyboard,
helper_->GetShowTooltipDelay(), {});
EXPECT_EQ(GetRootWindow(),
helper_->state_manager()->tooltip_parent_window());
EXPECT_EQ(expected_text, helper_->state_manager()->tooltip_text());
EXPECT_EQ(position, helper_->GetTooltipPosition());
EXPECT_FALSE(helper_->IsTooltipVisible());
EXPECT_TRUE(helper_->IsWillShowTooltipTimerRunning());
gfx::Point new_position = gfx::Point(10, 10);
// Because the tooltip was triggered by the keyboard, the position shouldn't
// be updated by a cursor triggered modification.
helper_->state_manager()->UpdatePositionIfNeeded(new_position,
TooltipTrigger::kCursor);
EXPECT_EQ(position, helper_->GetTooltipPosition());
// But it should be updated when the position's update is triggered by a
// keyboard action.
helper_->state_manager()->UpdatePositionIfNeeded(new_position,
TooltipTrigger::kKeyboard);
EXPECT_EQ(new_position, helper_->GetTooltipPosition());
// 2. Validate that we can't update the position when the timer isn't
// running.
helper_->HideAndReset();
position = new_position;
new_position = gfx::Point(20, 20);
helper_->state_manager()->UpdatePositionIfNeeded(new_position,
TooltipTrigger::kKeyboard);
EXPECT_EQ(position, helper_->GetTooltipPosition());
}
helper_->SkipTooltipShowDelay(true);
}
} // namespace views::corewm::test