| // Copyright 2014 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 "content/browser/web_contents/aura/overscroll_navigation_overlay.h" |
| |
| #include <vector> |
| #include "content/browser/frame_host/navigation_entry_impl.h" |
| #include "content/browser/web_contents/web_contents_view.h" |
| #include "content/common/frame_messages.h" |
| #include "content/common/view_messages.h" |
| #include "content/public/browser/overscroll_configuration.h" |
| #include "content/public/test/mock_render_process_host.h" |
| #include "content/test/test_render_frame_host.h" |
| #include "content/test/test_render_view_host.h" |
| #include "content/test/test_web_contents.h" |
| #include "ui/aura/test/test_windows.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura_extra/image_window_delegate.h" |
| #include "ui/compositor/scoped_animation_duration_scale_mode.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/compositor/test/layer_animator_test_controller.h" |
| #include "ui/events/gesture_detection/gesture_configuration.h" |
| #include "ui/events/test/event_generator.h" |
| #include "ui/gfx/codec/png_codec.h" |
| |
| namespace content { |
| |
| // A subclass of TestWebContents that offers a fake content window. |
| class OverscrollTestWebContents : public TestWebContents { |
| public: |
| ~OverscrollTestWebContents() override {} |
| |
| static OverscrollTestWebContents* Create( |
| BrowserContext* browser_context, |
| SiteInstance* instance, |
| scoped_ptr<aura::Window> fake_native_view, |
| scoped_ptr<aura::Window> fake_contents_window) { |
| OverscrollTestWebContents* web_contents = new OverscrollTestWebContents( |
| browser_context, fake_native_view.Pass(), fake_contents_window.Pass()); |
| web_contents->Init(WebContents::CreateParams(browser_context, instance)); |
| return web_contents; |
| } |
| |
| void ResetNativeView() { fake_native_view_.reset(); } |
| |
| void ResetContentNativeView() { fake_contents_window_.reset(); } |
| |
| void set_is_being_destroyed(bool val) { is_being_destroyed_ = val; } |
| |
| gfx::NativeView GetNativeView() override { return fake_native_view_.get(); } |
| |
| gfx::NativeView GetContentNativeView() override { |
| return fake_contents_window_.get(); |
| } |
| |
| bool IsBeingDestroyed() const override { return is_being_destroyed_; } |
| |
| protected: |
| explicit OverscrollTestWebContents( |
| BrowserContext* browser_context, |
| scoped_ptr<aura::Window> fake_native_view, |
| scoped_ptr<aura::Window> fake_contents_window) |
| : TestWebContents(browser_context), |
| fake_native_view_(fake_native_view.Pass()), |
| fake_contents_window_(fake_contents_window.Pass()), |
| is_being_destroyed_(false) {} |
| |
| private: |
| scoped_ptr<aura::Window> fake_native_view_; |
| scoped_ptr<aura::Window> fake_contents_window_; |
| bool is_being_destroyed_; |
| }; |
| |
| class OverscrollNavigationOverlayTest : public RenderViewHostImplTestHarness { |
| public: |
| OverscrollNavigationOverlayTest() |
| : first_("https://www.google.com"), |
| second_("http://www.chromium.org"), |
| third_("https://www.kernel.org/"), |
| fourth_("https://github.com/") {} |
| |
| ~OverscrollNavigationOverlayTest() override {} |
| |
| void SetDummyScreenshotOnNavEntry(NavigationEntry* entry) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(1, 1); |
| bitmap.eraseColor(SK_ColorWHITE); |
| std::vector<unsigned char> png_data; |
| gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, true, &png_data); |
| scoped_refptr<base::RefCountedBytes> png_bytes = |
| base::RefCountedBytes::TakeVector(&png_data); |
| NavigationEntryImpl* entry_impl = |
| NavigationEntryImpl::FromNavigationEntry(entry); |
| entry_impl->SetScreenshotPNGData(png_bytes); |
| } |
| |
| void ReceivePaintUpdate() { |
| FrameHostMsg_DidFirstVisuallyNonEmptyPaint msg( |
| main_test_rfh()->GetRoutingID()); |
| RenderViewHostTester::TestOnMessageReceived(test_rvh(), msg); |
| } |
| |
| void PerformBackNavigationViaSliderCallbacks() { |
| // Sets slide direction to BACK, sets screenshot from NavEntry at |
| // offset -1 on layer_delegate_. |
| scoped_ptr<aura::Window> window( |
| GetOverlay()->CreateBackWindow(GetBackSlideWindowBounds())); |
| bool window_created = window; |
| // Performs BACK navigation, sets image from layer_delegate_ on |
| // image_delegate_. |
| GetOverlay()->OnOverscrollCompleting(); |
| if (window_created) |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::BACK); |
| else |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::NONE); |
| window->SetBounds(gfx::Rect(root_window()->bounds().size())); |
| GetOverlay()->OnOverscrollCompleted(window.Pass()); |
| main_test_rfh()->PrepareForCommit(); |
| if (window_created) |
| EXPECT_TRUE(contents()->CrossProcessNavigationPending()); |
| else |
| EXPECT_FALSE(contents()->CrossProcessNavigationPending()); |
| } |
| |
| gfx::Rect GetFrontSlideWindowBounds() { |
| gfx::Rect bounds = gfx::Rect(root_window()->bounds().size()); |
| bounds.Offset(root_window()->bounds().size().width(), 0); |
| return bounds; |
| } |
| |
| gfx::Rect GetBackSlideWindowBounds() { |
| return gfx::Rect(root_window()->bounds().size()); |
| } |
| |
| // Const accessors. |
| const GURL first() { return first_; } |
| const GURL second() { return second_; } |
| const GURL third() { return third_; } |
| const GURL fourth() { return fourth_; } |
| |
| protected: |
| // RenderViewHostImplTestHarness: |
| void SetUp() override { |
| RenderViewHostImplTestHarness::SetUp(); |
| |
| // Set up the fake web contents native view. |
| scoped_ptr<aura::Window> fake_native_view(new aura::Window(nullptr)); |
| fake_native_view->Init(ui::LAYER_SOLID_COLOR); |
| root_window()->AddChild(fake_native_view.get()); |
| fake_native_view->SetBounds(gfx::Rect(root_window()->bounds().size())); |
| |
| // Set up the fake contents window. |
| scoped_ptr<aura::Window> fake_contents_window(new aura::Window(nullptr)); |
| fake_contents_window->Init(ui::LAYER_SOLID_COLOR); |
| root_window()->AddChild(fake_contents_window.get()); |
| fake_contents_window->SetBounds(gfx::Rect(root_window()->bounds().size())); |
| |
| // Replace the default test web contents with our custom class. |
| SetContents(OverscrollTestWebContents::Create( |
| browser_context(), |
| SiteInstance::Create(browser_context()), |
| fake_native_view.Pass(), |
| fake_contents_window.Pass())); |
| |
| contents()->NavigateAndCommit(first()); |
| EXPECT_TRUE(controller().GetVisibleEntry()); |
| EXPECT_FALSE(controller().CanGoBack()); |
| |
| contents()->NavigateAndCommit(second()); |
| EXPECT_TRUE(controller().CanGoBack()); |
| |
| contents()->NavigateAndCommit(third()); |
| EXPECT_TRUE(controller().CanGoBack()); |
| |
| contents()->NavigateAndCommit(fourth_); |
| EXPECT_TRUE(controller().CanGoBack()); |
| EXPECT_FALSE(controller().CanGoForward()); |
| |
| // Receive a paint update. This is necessary to make sure the size is set |
| // correctly in RenderWidgetHostImpl. |
| ViewHostMsg_UpdateRect_Params params; |
| memset(¶ms, 0, sizeof(params)); |
| params.view_size = gfx::Size(10, 10); |
| ViewHostMsg_UpdateRect rect(test_rvh()->GetRoutingID(), params); |
| RenderViewHostTester::TestOnMessageReceived(test_rvh(), rect); |
| |
| // Reset pending flags for size/paint. |
| test_rvh()->ResetSizeAndRepaintPendingFlags(); |
| |
| // Create the overlay, and set the contents of the overlay window. |
| overlay_.reset(new OverscrollNavigationOverlay(contents(), root_window())); |
| } |
| |
| void TearDown() override { |
| overlay_.reset(); |
| RenderViewHostImplTestHarness::TearDown(); |
| } |
| |
| OverscrollNavigationOverlay* GetOverlay() { |
| return overlay_.get(); |
| } |
| |
| private: |
| // Tests URLs. |
| const GURL first_; |
| const GURL second_; |
| const GURL third_; |
| const GURL fourth_; |
| |
| scoped_ptr<OverscrollNavigationOverlay> overlay_; |
| |
| DISALLOW_COPY_AND_ASSIGN(OverscrollNavigationOverlayTest); |
| }; |
| |
| // Tests that if a screenshot is available, it is set in the overlay window |
| // delegate. |
| TEST_F(OverscrollNavigationOverlayTest, WithScreenshot) { |
| SetDummyScreenshotOnNavEntry(controller().GetEntryAtOffset(-1)); |
| PerformBackNavigationViaSliderCallbacks(); |
| // Screenshot was set on NavEntry at offset -1. |
| EXPECT_TRUE(static_cast<aura_extra::ImageWindowDelegate*>( |
| GetOverlay()->window_->delegate())->has_image()); |
| } |
| |
| // Tests that if a screenshot is not available, no image is set in the overlay |
| // window delegate. |
| TEST_F(OverscrollNavigationOverlayTest, WithoutScreenshot) { |
| PerformBackNavigationViaSliderCallbacks(); |
| // No screenshot was set on NavEntry at offset -1. |
| EXPECT_FALSE(static_cast<aura_extra::ImageWindowDelegate*>( |
| GetOverlay()->window_->delegate())->has_image()); |
| } |
| |
| // Tests that if a navigation is attempted but there is nothing to navigate to, |
| // we return a null window. |
| TEST_F(OverscrollNavigationOverlayTest, CannotNavigate) { |
| EXPECT_EQ(GetOverlay()->CreateFrontWindow(GetFrontSlideWindowBounds()), |
| nullptr); |
| } |
| |
| // Tests that if a navigation is cancelled, no navigation is performed and the |
| // state is restored. |
| TEST_F(OverscrollNavigationOverlayTest, CancelNavigation) { |
| scoped_ptr<aura::Window> window = |
| GetOverlay()->CreateBackWindow(GetBackSlideWindowBounds()); |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::BACK); |
| |
| GetOverlay()->OnOverscrollCancelled(); |
| EXPECT_FALSE(contents()->CrossProcessNavigationPending()); |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::NONE); |
| } |
| |
| // Performs two navigations. The second navigation is cancelled, tests that the |
| // first one worked correctly. |
| TEST_F(OverscrollNavigationOverlayTest, CancelAfterSuccessfulNavigation) { |
| PerformBackNavigationViaSliderCallbacks(); |
| scoped_ptr<aura::Window> wrapper = |
| GetOverlay()->CreateBackWindow(GetBackSlideWindowBounds()); |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::BACK); |
| |
| GetOverlay()->OnOverscrollCancelled(); |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::NONE); |
| |
| EXPECT_TRUE(contents()->CrossProcessNavigationPending()); |
| NavigationEntry* pending = contents()->GetController().GetPendingEntry(); |
| main_test_rfh()->SendNavigate( |
| 0, pending->GetUniqueID(), false, pending->GetURL()); |
| EXPECT_EQ(contents()->GetURL(), third()); |
| } |
| |
| // Tests that an overscroll navigation that receives a paint update actually |
| // stops observing. |
| TEST_F(OverscrollNavigationOverlayTest, Navigation_PaintUpdate) { |
| PerformBackNavigationViaSliderCallbacks(); |
| ReceivePaintUpdate(); |
| |
| // Paint updates until the navigation is committed typically represent updates |
| // for the previous page, so we should still be observing. |
| EXPECT_TRUE(GetOverlay()->web_contents()); |
| |
| NavigationEntry* pending = contents()->GetController().GetPendingEntry(); |
| main_test_rfh()->SendNavigate( |
| 0, pending->GetUniqueID(), false, pending->GetURL()); |
| ReceivePaintUpdate(); |
| |
| // Navigation was committed and the paint update was received - we should no |
| // longer be observing. |
| EXPECT_FALSE(GetOverlay()->web_contents()); |
| EXPECT_EQ(contents()->GetURL(), third()); |
| } |
| |
| // Tests that an overscroll navigation that receives a loading update actually |
| // stops observing. |
| TEST_F(OverscrollNavigationOverlayTest, Navigation_LoadingUpdate) { |
| PerformBackNavigationViaSliderCallbacks(); |
| EXPECT_TRUE(GetOverlay()->web_contents()); |
| // DidStopLoading for any navigation should always reset the load flag and |
| // dismiss the overlay even if the pending navigation wasn't committed - |
| // this is a "safety net" in case we mis-identify the destination webpage |
| // (which can happen if a new navigation is performed while while a GestureNav |
| // navigation is in progress). |
| contents()->TestSetIsLoading(true); |
| contents()->TestSetIsLoading(false); |
| EXPECT_FALSE(GetOverlay()->web_contents()); |
| NavigationEntry* pending = contents()->GetController().GetPendingEntry(); |
| main_test_rfh()->SendNavigate( |
| 0, pending->GetUniqueID(), false, pending->GetURL()); |
| EXPECT_EQ(contents()->GetURL(), third()); |
| } |
| |
| TEST_F(OverscrollNavigationOverlayTest, CloseDuringAnimation) { |
| ui::ScopedAnimationDurationScaleMode normal_duration_( |
| ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); |
| GetOverlay()->owa_->OnOverscrollModeChange(OVERSCROLL_NONE, OVERSCROLL_EAST); |
| GetOverlay()->owa_->OnOverscrollComplete(OVERSCROLL_EAST); |
| EXPECT_EQ(GetOverlay()->direction_, OverscrollNavigationOverlay::BACK); |
| OverscrollTestWebContents* test_web_contents = |
| static_cast<OverscrollTestWebContents*>(web_contents()); |
| test_web_contents->set_is_being_destroyed(true); |
| test_web_contents->ResetContentNativeView(); |
| test_web_contents->ResetNativeView(); |
| // Ensure a clean close. |
| } |
| |
| |
| // Tests that swapping the overlay window at the end of a gesture caused by the |
| // start of a new overscroll does not crash and the events still reach the new |
| // overlay window. |
| TEST_F(OverscrollNavigationOverlayTest, OverlayWindowSwap) { |
| PerformBackNavigationViaSliderCallbacks(); |
| aura::Window* first_overlay_window = GetOverlay()->window_.get(); |
| EXPECT_TRUE(GetOverlay()->web_contents()); |
| EXPECT_TRUE(first_overlay_window); |
| |
| // At this stage, the overlay window is covering the web contents. Configure |
| // the animator of the overlay window for the test. |
| ui::ScopedAnimationDurationScaleMode normal_duration( |
| ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION); |
| ui::LayerAnimator* animator = GetOverlay()->window_->layer()->GetAnimator(); |
| animator->set_disable_timer_for_test(true); |
| ui::LayerAnimatorTestController test_controller(animator); |
| |
| int overscroll_complete_distance = |
| root_window()->bounds().size().width() * |
| content::GetOverscrollConfig( |
| content::OVERSCROLL_CONFIG_HORIZ_THRESHOLD_COMPLETE) + |
| ui::GestureConfiguration::GetInstance() |
| ->max_touch_move_in_pixels_for_click() + 1; |
| |
| // Start and complete a back navigation via a gesture. |
| ui::test::EventGenerator generator(root_window()); |
| generator.GestureScrollSequence(gfx::Point(0, 0), |
| gfx::Point(overscroll_complete_distance, 0), |
| base::TimeDelta::FromMilliseconds(10), |
| 10); |
| |
| ui::ScopedLayerAnimationSettings settings(animator); |
| test_controller.StartThreadedAnimationsIfNeeded(); |
| |
| // The overlay window should now be being animated to the edge of the screen. |
| // |first()overlay_window| is the back window. |
| // This is what the screen should look like. The X indicates where the next |
| // gesture starts for the test. |
| // +---------root_window--------+ |
| // |+-back window--+--front window--+ |
| // || | | | |
| // || 1 |X 2 | | |
| // || | | | |
| // |+--------------+------------|---+ |
| // +----------------------------+ |
| // | overscroll || |
| // | complete || |
| // | distance || |
| // |<------------->|| |
| // | second | |
| // | overscroll | |
| // | start distance | |
| // |<-------------->| |
| EXPECT_EQ(GetOverlay()->window_.get(), first_overlay_window); |
| |
| // The overlay window is halfway through, start another animation that will |
| // cancel the first one. The event that cancels the animation will go to |
| // the slide window, which will be used as the overlay window when the new |
| // overscroll starts. |
| int second_overscroll_start_distance = overscroll_complete_distance + 1; |
| generator.GestureScrollSequence( |
| gfx::Point(second_overscroll_start_distance, 0), |
| gfx::Point( |
| second_overscroll_start_distance + overscroll_complete_distance, 0), |
| base::TimeDelta::FromMilliseconds(10), 10); |
| EXPECT_TRUE(GetOverlay()->window_.get()); |
| // The overlay window should be a new window. |
| EXPECT_NE(GetOverlay()->window_.get(), first_overlay_window); |
| |
| // Complete the animation. |
| GetOverlay()->window_->layer()->GetAnimator()->StopAnimating(); |
| EXPECT_TRUE(GetOverlay()->window_.get()); |
| |
| // Load the page. |
| contents()->CommitPendingNavigation(); |
| ReceivePaintUpdate(); |
| EXPECT_FALSE(GetOverlay()->window_.get()); |
| EXPECT_EQ(contents()->GetURL(), first()); |
| } |
| |
| } // namespace content |