| // Copyright 2021 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 "ui/views/animation/bubble_slide_animator.h" |
| |
| #include <memory> |
| |
| #include "base/test/bind.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/gfx/animation/animation_test_api.h" |
| #include "ui/views/bubble/bubble_dialog_delegate_view.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/flex_layout_view.h" |
| #include "ui/views/test/widget_test.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kSlideDuration = |
| base::TimeDelta::FromMilliseconds(1000); |
| constexpr base::TimeDelta kHalfSlideDuration = kSlideDuration / 2; |
| |
| // This will be the size of the three horizontally-oriented anchor views as well |
| // as the target size for the floating view. |
| constexpr gfx::Size kTestViewSize(100, 100); |
| // Make this big enough that even if we anchor to a third view horizontally, no |
| // mirroring should happen. |
| constexpr gfx::Rect kAnchorWidgetRect(50, 50, 400, 250); |
| |
| class TestBubbleView : public BubbleDialogDelegateView { |
| public: |
| explicit TestBubbleView(View* anchor_view) |
| : BubbleDialogDelegateView(anchor_view, BubbleBorder::TOP_LEFT) { |
| SetButtons(ui::DIALOG_BUTTON_NONE); |
| SetLayoutManager(std::make_unique<FillLayout>()); |
| AddChildView(std::make_unique<View>())->SetPreferredSize(kTestViewSize); |
| } |
| |
| protected: |
| void AddedToWidget() override { |
| BubbleDialogDelegateView::AddedToWidget(); |
| SizeToContents(); |
| } |
| }; |
| |
| class TestBubbleSlideAnimator : public BubbleSlideAnimator { |
| public: |
| using BubbleSlideAnimator::BubbleSlideAnimator; |
| ~TestBubbleSlideAnimator() override = default; |
| |
| void AnimationContainerWasSet(gfx::AnimationContainer* container) override { |
| BubbleSlideAnimator::AnimationContainerWasSet(container); |
| container_test_api_.reset(); |
| if (container) { |
| container_test_api_ = |
| std::make_unique<gfx::AnimationContainerTestApi>(container); |
| } |
| } |
| |
| gfx::AnimationContainerTestApi* test_api() { |
| return container_test_api_.get(); |
| } |
| |
| private: |
| std::unique_ptr<gfx::AnimationContainerTestApi> container_test_api_; |
| }; |
| |
| } // namespace |
| |
| class BubbleSlideAnimatorTest : public test::WidgetTest { |
| public: |
| void SetUp() override { |
| test::WidgetTest::SetUp(); |
| anchor_widget_ = CreateTestWidget(Widget::InitParams::Type::TYPE_WINDOW); |
| auto* const contents_view = anchor_widget_->GetRootView()->AddChildView( |
| std::make_unique<FlexLayoutView>()); |
| contents_view->SetOrientation(LayoutOrientation::kHorizontal); |
| contents_view->SetMainAxisAlignment(LayoutAlignment::kStart); |
| contents_view->SetCrossAxisAlignment(LayoutAlignment::kStart); |
| view1_ = contents_view->AddChildView(std::make_unique<View>()); |
| view2_ = contents_view->AddChildView(std::make_unique<View>()); |
| view3_ = contents_view->AddChildView(std::make_unique<View>()); |
| view1_->SetPreferredSize(kTestViewSize); |
| view2_->SetPreferredSize(kTestViewSize); |
| view3_->SetPreferredSize(kTestViewSize); |
| anchor_widget_->Show(); |
| anchor_widget_->SetBounds(kAnchorWidgetRect); |
| bubble_ = new TestBubbleView(view1_); |
| widget_ = BubbleDialogDelegateView::CreateBubble(bubble_); |
| delegate_ = std::make_unique<TestBubbleSlideAnimator>(bubble_); |
| delegate_->SetSlideDuration(kSlideDuration); |
| } |
| |
| void TearDown() override { |
| CloseWidget(); |
| if (anchor_widget_ && !anchor_widget_->IsClosed()) |
| anchor_widget_->CloseNow(); |
| test::WidgetTest::TearDown(); |
| } |
| |
| void CloseWidget() { |
| if (widget_ && !widget_->IsClosed()) |
| widget_->CloseNow(); |
| widget_ = nullptr; |
| bubble_ = nullptr; |
| } |
| |
| protected: |
| std::unique_ptr<Widget> anchor_widget_; |
| BubbleDialogDelegateView* bubble_ = nullptr; |
| Widget* widget_ = nullptr; |
| View* view1_; |
| View* view2_; |
| View* view3_; |
| std::unique_ptr<TestBubbleSlideAnimator> delegate_; |
| }; |
| |
| TEST_F(BubbleSlideAnimatorTest, InitiateSlide) { |
| const auto bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->AnimateToAnchorView(view2_); |
| // Shouldn't animate from here yet. |
| EXPECT_EQ(bounds, widget_->GetWindowBoundsInScreen()); |
| EXPECT_TRUE(delegate_->is_animating()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, SlideProgresses) { |
| const auto starting_bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| const auto intermediate_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_TRUE(delegate_->is_animating()); |
| EXPECT_EQ(intermediate_bounds.y(), starting_bounds.y()); |
| EXPECT_GT(intermediate_bounds.x(), starting_bounds.x()); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| const auto final_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| EXPECT_EQ(final_bounds.y(), starting_bounds.y()); |
| EXPECT_GT(final_bounds.x(), intermediate_bounds.x()); |
| EXPECT_EQ(final_bounds.x(), starting_bounds.x() + view2_->x() - view1_->x()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, SnapToAnchorView) { |
| const auto starting_bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->SnapToAnchorView(view2_); |
| const auto final_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| EXPECT_EQ(final_bounds.y(), starting_bounds.y()); |
| EXPECT_EQ(final_bounds.x(), starting_bounds.x() + view2_->x() - view1_->x()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, SlideCallbacksCalled) { |
| int progress_count = 0; |
| int complete_count = 0; |
| double last_progress = 0.0; |
| auto progress_sub = delegate_->AddSlideProgressedCallback( |
| base::BindLambdaForTesting([&](BubbleSlideAnimator*, double progress) { |
| last_progress = progress; |
| ++progress_count; |
| })); |
| auto completed_sub = |
| delegate_->AddSlideCompleteCallback(base::BindLambdaForTesting( |
| [&](BubbleSlideAnimator*) { ++complete_count; })); |
| delegate_->AnimateToAnchorView(view2_); |
| EXPECT_EQ(0, progress_count); |
| EXPECT_EQ(0, complete_count); |
| EXPECT_EQ(0.0, last_progress); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| EXPECT_EQ(1, progress_count); |
| EXPECT_EQ(0, complete_count); |
| EXPECT_GT(last_progress, 0.0); |
| EXPECT_LT(last_progress, 1.0); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| EXPECT_EQ(2, progress_count); |
| EXPECT_EQ(1, complete_count); |
| EXPECT_EQ(1.0, last_progress); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, SnapCallbacksCalled) { |
| int progress_count = 0; |
| int complete_count = 0; |
| double last_progress = 0.0; |
| auto progress_sub = delegate_->AddSlideProgressedCallback( |
| base::BindLambdaForTesting([&](BubbleSlideAnimator*, double progress) { |
| last_progress = progress; |
| ++progress_count; |
| })); |
| auto completed_sub = |
| delegate_->AddSlideCompleteCallback(base::BindLambdaForTesting( |
| [&](BubbleSlideAnimator*) { ++complete_count; })); |
| delegate_->SnapToAnchorView(view2_); |
| EXPECT_EQ(1, progress_count); |
| EXPECT_EQ(1, complete_count); |
| EXPECT_EQ(1.0, last_progress); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, InterruptingWithSlideCallsCorrectCallbacks) { |
| int progress_count = 0; |
| int complete_count = 0; |
| double last_progress = 0.0; |
| auto progress_sub = delegate_->AddSlideProgressedCallback( |
| base::BindLambdaForTesting([&](BubbleSlideAnimator*, double progress) { |
| last_progress = progress; |
| ++progress_count; |
| })); |
| auto completed_sub = |
| delegate_->AddSlideCompleteCallback(base::BindLambdaForTesting( |
| [&](BubbleSlideAnimator*) { ++complete_count; })); |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| EXPECT_EQ(1, progress_count); |
| EXPECT_EQ(0, complete_count); |
| delegate_->AnimateToAnchorView(view3_); |
| EXPECT_EQ(1, progress_count); |
| EXPECT_EQ(0, complete_count); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| EXPECT_EQ(2, progress_count); |
| EXPECT_EQ(1, complete_count); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, InterruptingWithSnapCallsCorrectCallbacks) { |
| int progress_count = 0; |
| int complete_count = 0; |
| double last_progress = 0.0; |
| auto progress_sub = delegate_->AddSlideProgressedCallback( |
| base::BindLambdaForTesting([&](BubbleSlideAnimator*, double progress) { |
| last_progress = progress; |
| ++progress_count; |
| })); |
| auto completed_sub = |
| delegate_->AddSlideCompleteCallback(base::BindLambdaForTesting( |
| [&](BubbleSlideAnimator*) { ++complete_count; })); |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| EXPECT_EQ(1, progress_count); |
| EXPECT_EQ(0, complete_count); |
| delegate_->SnapToAnchorView(view3_); |
| EXPECT_EQ(2, progress_count); |
| EXPECT_EQ(1, complete_count); |
| EXPECT_EQ(1.0, last_progress); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, CancelAnimation) { |
| int progress_count = 0; |
| int complete_count = 0; |
| double last_progress = 0.0; |
| auto progress_sub = delegate_->AddSlideProgressedCallback( |
| base::BindLambdaForTesting([&](BubbleSlideAnimator*, double progress) { |
| last_progress = progress; |
| ++progress_count; |
| })); |
| auto completed_sub = |
| delegate_->AddSlideCompleteCallback(base::BindLambdaForTesting( |
| [&](BubbleSlideAnimator*) { ++complete_count; })); |
| |
| const auto initial_bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| const auto second_bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->AnimateToAnchorView(view1_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| const auto final_bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->StopAnimation(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| EXPECT_EQ(2, progress_count); |
| EXPECT_EQ(1, complete_count); |
| EXPECT_GT(last_progress, 0.0); |
| EXPECT_LT(last_progress, 1.0); |
| EXPECT_GT(final_bounds.x(), initial_bounds.x()); |
| EXPECT_LT(final_bounds.x(), second_bounds.x()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, MultipleSlidesInSequence) { |
| // First slide. |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| const auto first_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| |
| // Second slide. |
| delegate_->AnimateToAnchorView(view3_); |
| EXPECT_TRUE(delegate_->is_animating()); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| |
| // Ensure we are sliding. |
| const auto intermediate_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_TRUE(delegate_->is_animating()); |
| EXPECT_EQ(intermediate_bounds.y(), first_bounds.y()); |
| EXPECT_GT(intermediate_bounds.x(), first_bounds.x()); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| |
| // Ensure we're done. |
| const auto final_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| EXPECT_EQ(final_bounds.y(), first_bounds.y()); |
| EXPECT_EQ(final_bounds.x(), first_bounds.x() + view3_->x() - view2_->x()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, SlideBackToStartingPosition) { |
| const auto first_bounds = widget_->GetWindowBoundsInScreen(); |
| delegate_->AnimateToAnchorView(view3_); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| delegate_->AnimateToAnchorView(view1_); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| const auto final_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| EXPECT_EQ(final_bounds, first_bounds); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, InterruptingSlide) { |
| const auto starting_bounds = widget_->GetWindowBoundsInScreen(); |
| |
| // Start the first slide. |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| const auto intermediate_bounds1 = widget_->GetWindowBoundsInScreen(); |
| EXPECT_TRUE(delegate_->is_animating()); |
| |
| // Interrupt mid-slide with another slide. |
| delegate_->AnimateToAnchorView(view3_); |
| EXPECT_TRUE(delegate_->is_animating()); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| |
| // Ensure we are sliding. |
| const auto intermediate_bounds2 = widget_->GetWindowBoundsInScreen(); |
| EXPECT_TRUE(delegate_->is_animating()); |
| EXPECT_EQ(intermediate_bounds2.y(), intermediate_bounds1.y()); |
| EXPECT_GT(intermediate_bounds2.x(), intermediate_bounds1.x()); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| |
| // Ensure we are done. |
| const auto final_bounds = widget_->GetWindowBoundsInScreen(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| EXPECT_EQ(final_bounds.y(), starting_bounds.y()); |
| EXPECT_EQ(final_bounds.x(), starting_bounds.x() + view3_->x() - view1_->x()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, WidgetClosedDuringSlide) { |
| delegate_->AnimateToAnchorView(view2_); |
| CloseWidget(); |
| EXPECT_FALSE(delegate_->is_animating()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, AnimatorDestroyedDuringSlide) { |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| delegate_.reset(); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, AnimationSetsAnchorView) { |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| EXPECT_EQ(view2_, bubble_->GetAnchorView()); |
| delegate_->AnimateToAnchorView(view3_); |
| delegate_->test_api()->IncrementTime(kSlideDuration); |
| EXPECT_EQ(view3_, bubble_->GetAnchorView()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, SnapSetsAnchorView) { |
| delegate_->SnapToAnchorView(view2_); |
| EXPECT_EQ(view2_, bubble_->GetAnchorView()); |
| delegate_->SnapToAnchorView(view3_); |
| EXPECT_EQ(view3_, bubble_->GetAnchorView()); |
| } |
| |
| TEST_F(BubbleSlideAnimatorTest, CancelDoesntSetAnchorView) { |
| delegate_->AnimateToAnchorView(view2_); |
| delegate_->test_api()->IncrementTime(kHalfSlideDuration); |
| delegate_->StopAnimation(); |
| EXPECT_EQ(view1_, bubble_->GetAnchorView()); |
| } |
| |
| } // namespace views |