blob: 45a899d206f2007157213b0e7a4098e2ed628950 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/location_bar/icon_label_bubble_view.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/animation/test/ink_drop_host_view_test_api.h"
#include "ui/views/animation/test/test_ink_drop.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/widget/widget_utils.h"
#if defined(OS_CHROMEOS)
#include "ui/aura/window.h"
#endif
using views::test::InkDropHostViewTestApi;
using views::test::TestInkDrop;
namespace {
const int kStayOpenTimeMS = 100;
const int kOpenTimeMS = 100;
const int kAnimationDurationMS = (kOpenTimeMS * 2) + kStayOpenTimeMS;
const int kImageSize = 15;
const SkColor kTestColor = SkColorSetRGB(64, 64, 64);
const int kNumberOfSteps = 300;
class TestIconLabelBubbleView : public IconLabelBubbleView {
public:
using IconLabelBubbleView::AnimateIn;
using IconLabelBubbleView::AnimateOut;
using IconLabelBubbleView::ResetSlideAnimation;
enum State {
GROWING,
STEADY,
SHRINKING,
};
explicit TestIconLabelBubbleView(const gfx::FontList& font_list)
: IconLabelBubbleView(font_list), value_(0), is_bubble_showing_(false) {
GetImageView()->SetImageSize(gfx::Size(kImageSize, kImageSize));
SetLabel(base::ASCIIToUTF16("Label"));
separator_view()->set_disable_animation_for_test(true);
}
void SetCurrentAnimationValue(int value) {
value_ = value;
SizeToPreferredSize();
}
int width() const { return bounds().width(); }
bool IsLabelVisible() const { return label()->visible(); }
void SetLabelVisible(bool visible) { label()->SetVisible(visible); }
const gfx::Rect& GetLabelBounds() const { return label()->bounds(); }
State state() const {
const double kOpenFraction =
static_cast<double>(kOpenTimeMS) / kAnimationDurationMS;
double state = value_ / (double)kNumberOfSteps;
if (state < kOpenFraction)
return GROWING;
if (state > (1.0 - kOpenFraction))
return SHRINKING;
return STEADY;
}
void HideBubble() {
AnimateInkDrop(views::InkDropState::HIDDEN, nullptr /* event */);
is_bubble_showing_ = false;
}
bool IsBubbleShowing() const override { return is_bubble_showing_; }
protected:
// IconLabelBubbleView:
SkColor GetTextColor() const override { return kTestColor; }
SkColor GetInkDropBaseColor() const override { return kTestColor; }
bool ShouldShowLabel() const override {
return !IsShrinking() ||
(width() >
(image()->GetPreferredSize().width() +
GetLayoutInsets(LOCATION_BAR_ICON_INTERIOR_PADDING).width() +
2 * GetLayoutConstant(LOCATION_BAR_ELEMENT_PADDING)));
}
double WidthMultiplier() const override {
const double kOpenFraction =
static_cast<double>(kOpenTimeMS) / kAnimationDurationMS;
double fraction = value_ / (double)kNumberOfSteps;
switch (state()) {
case GROWING:
return fraction / kOpenFraction;
case STEADY:
return 1.0;
case SHRINKING:
return (1.0 - fraction) / kOpenFraction;
}
NOTREACHED();
return 1.0;
}
bool IsShrinking() const override { return state() == SHRINKING; }
bool ShowBubble(const ui::Event& event) override {
AnimateInkDrop(views::InkDropState::ACTIVATED, nullptr /* event */);
is_bubble_showing_ = true;
return true;
}
private:
int value_;
bool is_bubble_showing_;
DISALLOW_COPY_AND_ASSIGN(TestIconLabelBubbleView);
};
} // namespace
class IconLabelBubbleViewTest : public ChromeViewsTestBase {
protected:
// ChromeViewsTestBase:
void SetUp() override {
ChromeViewsTestBase::SetUp();
gfx::FontList font_list;
CreateWidget();
generator_ =
std::make_unique<ui::test::EventGenerator>(GetRootWindow(widget_));
view_ = new TestIconLabelBubbleView(font_list);
view_->SetBoundsRect(gfx::Rect(0, 0, 24, 24));
widget_->SetContentsView(view_);
widget_->Show();
}
void TearDown() override {
generator_.reset();
if (widget_ && !widget_->IsClosed())
widget_->Close();
ChromeViewsTestBase::TearDown();
}
void VerifyWithAnimationStep(int step) {
Reset();
for (int value = 0; value < kNumberOfSteps; value += step) {
SetValue(value);
VerifyAnimationStep();
}
view_->SetLabelVisible(false);
}
TestInkDrop* ink_drop() { return ink_drop_; }
TestIconLabelBubbleView* view() { return view_; }
ui::test::EventGenerator* generator() { return generator_.get(); }
void AttachInkDrop() {
ink_drop_ = new TestInkDrop();
InkDropHostViewTestApi(view_).SetInkDrop(base::WrapUnique(ink_drop_));
}
private:
void CreateWidget() {
DCHECK(!widget_);
widget_ = new views::Widget;
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.bounds = gfx::Rect(0, 0, 200, 200);
widget_->Init(params);
}
void Reset() {
view_->SetLabelVisible(true);
SetValue(0);
steady_reached_ = false;
shrinking_reached_ = false;
minimum_size_reached_ = false;
previous_width_ = 0;
initial_image_x_ = GetImageBounds().x();
EXPECT_EQ(GetLayoutInsets(LOCATION_BAR_ICON_INTERIOR_PADDING).left(),
initial_image_x_);
}
void VerifyAnimationStep() {
switch (state()) {
case TestIconLabelBubbleView::State::GROWING: {
EXPECT_GE(width(), previous_width_);
EXPECT_EQ(initial_image_x_, GetImageBounds().x());
EXPECT_GE(GetImageBounds().x(), 0);
if (GetImageBounds().width() > 0)
EXPECT_LE(GetImageBounds().right(), width());
EXPECT_TRUE(IsLabelVisible());
if (GetLabelBounds().width() > 0) {
EXPECT_GT(GetLabelBounds().x(), GetImageBounds().right());
EXPECT_LT(GetLabelBounds().right(), width());
}
break;
}
case TestIconLabelBubbleView::State::STEADY: {
if (steady_reached_)
EXPECT_EQ(previous_width_, width());
EXPECT_EQ(initial_image_x_, GetImageBounds().x());
EXPECT_LT(GetImageBounds().right(), width());
EXPECT_TRUE(IsLabelVisible());
EXPECT_GT(GetLabelBounds().x(), GetImageBounds().right());
EXPECT_LT(GetLabelBounds().right(), width());
steady_reached_ = true;
break;
}
case TestIconLabelBubbleView::State::SHRINKING: {
if (shrinking_reached_)
EXPECT_LE(width(), previous_width_);
if (minimum_size_reached_)
EXPECT_EQ(previous_width_, width());
EXPECT_GE(GetImageBounds().x(), 0);
if (width() <= initial_image_x_ + kImageSize) {
EXPECT_EQ(width(), GetImageBounds().right());
EXPECT_EQ(0, GetLabelBounds().width());
} else {
EXPECT_EQ(initial_image_x_, GetImageBounds().x());
EXPECT_LE(GetImageBounds().right(), width());
}
if (GetLabelBounds().width() > 0) {
EXPECT_GT(GetLabelBounds().x(), GetImageBounds().right());
EXPECT_LT(GetLabelBounds().right(), width());
}
shrinking_reached_ = true;
if (width() == kImageSize)
minimum_size_reached_ = true;
break;
}
}
previous_width_ = width();
}
void SetValue(int value) { view_->SetCurrentAnimationValue(value); }
TestIconLabelBubbleView::State state() const { return view_->state(); }
int width() { return view_->width(); }
bool IsLabelVisible() { return view_->IsLabelVisible(); }
const gfx::Rect& GetLabelBounds() const { return view_->GetLabelBounds(); }
const gfx::Rect& GetImageBounds() const {
return view_->GetImageView()->bounds();
}
views::Widget* widget_ = nullptr;
TestIconLabelBubbleView* view_ = nullptr;
TestInkDrop* ink_drop_ = nullptr;
std::unique_ptr<ui::test::EventGenerator> generator_;
bool steady_reached_ = false;
bool shrinking_reached_ = false;
bool minimum_size_reached_ = false;
int previous_width_ = 0;
int initial_image_x_ = 0;
};
// Tests layout rules for IconLabelBubbleView while simulating animation.
// The animation is first growing the bubble from zero, then keeping its size
// constant and finally shrinking it down to its minimum size which is the image
// size.
// Various step sizes during animation simulate different possible timing.
TEST_F(IconLabelBubbleViewTest, AnimateLayout) {
VerifyWithAnimationStep(1);
VerifyWithAnimationStep(5);
VerifyWithAnimationStep(10);
VerifyWithAnimationStep(25);
}
// Verify that clicking the view a second time hides its bubble.
TEST_F(IconLabelBubbleViewTest, SecondClick) {
generator()->PressLeftButton();
EXPECT_FALSE(view()->IsBubbleShowing());
generator()->ReleaseLeftButton();
EXPECT_TRUE(view()->IsBubbleShowing());
// Hide the bubble manually. In the browser this would normally happen during
// the event processing.
generator()->PressLeftButton();
view()->HideBubble();
EXPECT_FALSE(view()->IsBubbleShowing());
generator()->ReleaseLeftButton();
}
TEST_F(IconLabelBubbleViewTest, MouseInkDropState) {
AttachInkDrop();
generator()->PressLeftButton();
EXPECT_EQ(views::InkDropState::ACTION_PENDING,
ink_drop()->GetTargetInkDropState());
generator()->ReleaseLeftButton();
EXPECT_EQ(views::InkDropState::ACTIVATED,
ink_drop()->GetTargetInkDropState());
view()->HideBubble();
EXPECT_EQ(views::InkDropState::HIDDEN, ink_drop()->GetTargetInkDropState());
// If the bubble is shown, the InkDropState should not change to
// ACTION_PENDING.
generator()->PressLeftButton();
EXPECT_EQ(views::InkDropState::ACTION_PENDING,
ink_drop()->GetTargetInkDropState());
generator()->ReleaseLeftButton();
EXPECT_EQ(views::InkDropState::ACTIVATED,
ink_drop()->GetTargetInkDropState());
generator()->PressLeftButton();
EXPECT_NE(views::InkDropState::ACTION_PENDING,
ink_drop()->GetTargetInkDropState());
}
// Tests the separator opacity. The separator should disappear when there's
// an ink drop. Otherwise, it should be visible.
TEST_F(IconLabelBubbleViewTest, SeparatorOpacity) {
views::View* separator_view = view()->separator_view();
separator_view->SetPaintToLayer();
view()->SetLabel(base::ASCIIToUTF16("x"));
EXPECT_EQ(1.0f, separator_view->layer()->opacity());
AttachInkDrop();
generator()->PressLeftButton();
view()->InkDropAnimationStarted();
EXPECT_EQ(views::InkDropState::ACTION_PENDING,
ink_drop()->GetTargetInkDropState());
EXPECT_EQ(0.0f, separator_view->layer()->opacity());
generator()->ReleaseLeftButton();
EXPECT_EQ(views::InkDropState::ACTIVATED,
ink_drop()->GetTargetInkDropState());
EXPECT_EQ(0.0f, separator_view->layer()->opacity());
view()->HideBubble();
view()->InkDropAnimationStarted();
EXPECT_EQ(views::InkDropState::HIDDEN, ink_drop()->GetTargetInkDropState());
EXPECT_EQ(1.0f, separator_view->layer()->opacity());
}
#if !defined(OS_MACOSX)
TEST_F(IconLabelBubbleViewTest, GestureInkDropState) {
AttachInkDrop();
generator()->GestureTapAt(gfx::Point());
EXPECT_EQ(views::InkDropState::ACTIVATED,
ink_drop()->GetTargetInkDropState());
view()->HideBubble();
EXPECT_EQ(views::InkDropState::HIDDEN, ink_drop()->GetTargetInkDropState());
// If the bubble is shown, the InkDropState should not change to
// ACTIVATED.
generator()->GestureTapAt(gfx::Point());
EXPECT_EQ(views::InkDropState::ACTIVATED,
ink_drop()->GetTargetInkDropState());
generator()->GestureTapAt(gfx::Point());
view()->HideBubble();
EXPECT_EQ(views::InkDropState::HIDDEN, ink_drop()->GetTargetInkDropState());
}
#endif
TEST_F(IconLabelBubbleViewTest, LabelVisibilityAfterAnimation) {
view()->AnimateIn(base::nullopt);
EXPECT_TRUE(view()->IsLabelVisible());
view()->AnimateOut();
EXPECT_FALSE(view()->IsLabelVisible());
// Label should reappear if animated in after being animated out.
view()->AnimateIn(base::nullopt);
EXPECT_TRUE(view()->IsLabelVisible());
}
TEST_F(IconLabelBubbleViewTest, LabelVisibilityAfterAnimationReset) {
view()->ResetSlideAnimation(true);
EXPECT_TRUE(view()->IsLabelVisible());
view()->ResetSlideAnimation(false);
EXPECT_FALSE(view()->IsLabelVisible());
// Label should reappear if animated in after being reset out.
view()->AnimateIn(base::nullopt);
EXPECT_TRUE(view()->IsLabelVisible());
}
TEST_F(IconLabelBubbleViewTest,
LabelVisibilityAfterAnimationWithDefinedString) {
view()->AnimateIn(IDS_AUTOFILL_CARD_SAVED);
EXPECT_TRUE(view()->IsLabelVisible());
view()->AnimateOut();
EXPECT_FALSE(view()->IsLabelVisible());
// Label should reappear if animated in after being animated out.
view()->AnimateIn(IDS_AUTOFILL_CARD_SAVED);
EXPECT_TRUE(view()->IsLabelVisible());
}
#if defined(OS_CHROMEOS)
// Verifies IconLabelBubbleView::CalculatePreferredSize() doesn't crash when
// there is a widget but no compositor.
using IconLabelBubbleViewCrashTest = ChromeViewsTestBase;
TEST_F(IconLabelBubbleViewCrashTest,
GetPreferredSizeDoesntCrashWhenNoCompositor) {
gfx::FontList font_list;
views::Widget::InitParams params =
CreateParams(views::Widget::InitParams::TYPE_WINDOW);
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
views::Widget widget;
widget.Init(params);
IconLabelBubbleView* icon_label_bubble_view =
new TestIconLabelBubbleView(font_list);
icon_label_bubble_view->SetLabel(base::ASCIIToUTF16("x"));
widget.GetContentsView()->AddChildView(icon_label_bubble_view);
aura::Window* widget_native_view = widget.GetNativeView();
// Remove the window from its parent. This means GetWidget() in
// IconLabelBubbleView will return non-null, but GetWidget()->GetCompositor()
// will return null.
ASSERT_TRUE(widget_native_view->parent());
widget_native_view->parent()->RemoveChild(widget_native_view);
static_cast<views::View*>(icon_label_bubble_view)->GetPreferredSize();
}
#endif