| // Copyright 2018 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/renderer_host/direct_manipulation_helper_win.h" |
| |
| #include "base/win/windows_version.h" |
| #include "content/browser/renderer_host/direct_manipulation_test_helper_win.h" |
| #include "content/browser/renderer_host/legacy_render_widget_host_win.h" |
| #include "content/browser/renderer_host/render_widget_host_view_aura.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/base/win/window_event_target.h" |
| #include "ui/events/event_rewriter.h" |
| #include "ui/events/event_source.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| |
| class DirectManipulationBrowserTestBase : public ContentBrowserTest { |
| public: |
| DirectManipulationBrowserTestBase() {} |
| |
| LegacyRenderWidgetHostHWND* GetLegacyRenderWidgetHostHWND() { |
| RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>( |
| shell()->web_contents()->GetRenderWidgetHostView()); |
| return rwhva->legacy_render_widget_host_HWND_; |
| } |
| |
| ui::WindowEventTarget* GetWindowEventTarget() { |
| LegacyRenderWidgetHostHWND* lrwhh = GetLegacyRenderWidgetHostHWND(); |
| |
| return lrwhh->GetWindowEventTarget(lrwhh->GetParent()); |
| } |
| |
| void SetDirectManipulationInteraction( |
| DIRECTMANIPULATION_INTERACTION_TYPE type) { |
| LegacyRenderWidgetHostHWND* lrwhh = GetLegacyRenderWidgetHostHWND(); |
| |
| lrwhh->direct_manipulation_helper_->event_handler_->OnInteraction(nullptr, |
| type); |
| } |
| |
| bool HasAnimationObserver(LegacyRenderWidgetHostHWND* lrwhh) { |
| return lrwhh->direct_manipulation_helper_->compositor_ |
| ->HasAnimationObserver(lrwhh->direct_manipulation_helper_.get()); |
| } |
| |
| void StartNewSequence() { |
| LegacyRenderWidgetHostHWND* lrwhh = GetLegacyRenderWidgetHostHWND(); |
| |
| lrwhh->direct_manipulation_helper_->event_handler_->OnViewportStatusChanged( |
| lrwhh->direct_manipulation_helper_->viewport_.Get(), |
| DIRECTMANIPULATION_READY, DIRECTMANIPULATION_RUNNING); |
| } |
| |
| void UpdateContents(MockDirectManipulationContent* content) { |
| LegacyRenderWidgetHostHWND* lrwhh = GetLegacyRenderWidgetHostHWND(); |
| lrwhh->direct_manipulation_helper_->event_handler_->OnContentUpdated( |
| lrwhh->direct_manipulation_helper_->viewport_.Get(), content); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DirectManipulationBrowserTestBase); |
| }; |
| |
| class DirectManipulationBrowserTest : public DirectManipulationBrowserTestBase { |
| public: |
| DirectManipulationBrowserTest() {} |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DirectManipulationBrowserTest); |
| }; |
| |
| // Ensure the AnimationObserver is only created after direct manipulation |
| // interaction begin and destroyed after direct manipulation interaction end. |
| IN_PROC_BROWSER_TEST_F(DirectManipulationBrowserTest, |
| ObserverDuringInteraction) { |
| if (base::win::GetVersion() < base::win::Version::WIN10) |
| return; |
| |
| EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); |
| |
| LegacyRenderWidgetHostHWND* lrwhh = GetLegacyRenderWidgetHostHWND(); |
| ASSERT_TRUE(lrwhh); |
| |
| // The observer should not be created before it is needed. |
| EXPECT_FALSE(HasAnimationObserver(lrwhh)); |
| |
| // Begin direct manipulation interaction. |
| SetDirectManipulationInteraction(DIRECTMANIPULATION_INTERACTION_BEGIN); |
| // AnimationObserver should be added after direct manipulation interaction |
| // begin. |
| EXPECT_TRUE(HasAnimationObserver(lrwhh)); |
| |
| // End direct manipulation interaction. |
| SetDirectManipulationInteraction(DIRECTMANIPULATION_INTERACTION_END); |
| |
| // The animation observer should be removed. |
| EXPECT_FALSE(HasAnimationObserver(lrwhh)); |
| } |
| |
| // EventLogger is to observe the events sent from WindowEventTarget (the root |
| // window). |
| class EventLogger : public ui::EventRewriter { |
| public: |
| EventLogger() {} |
| ~EventLogger() override {} |
| |
| std::unique_ptr<ui::Event> ReleaseLastEvent() { |
| return std::move(last_event_); |
| } |
| |
| private: |
| // ui::EventRewriter |
| ui::EventDispatchDetails RewriteEvent( |
| const ui::Event& event, |
| const Continuation continuation) override { |
| DCHECK(!last_event_); |
| last_event_ = ui::Event::Clone(event); |
| return SendEvent(continuation, &event); |
| } |
| |
| std::unique_ptr<ui::Event> last_event_ = nullptr; |
| |
| DISALLOW_COPY_AND_ASSIGN(EventLogger); |
| }; |
| |
| // Check DirectManipulation events convert to ui::event correctly. |
| IN_PROC_BROWSER_TEST_F(DirectManipulationBrowserTest, EventConvert) { |
| if (base::win::GetVersion() < base::win::Version::WIN10) |
| return; |
| |
| EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); |
| |
| LegacyRenderWidgetHostHWND* lrwhh = GetLegacyRenderWidgetHostHWND(); |
| ASSERT_TRUE(lrwhh); |
| |
| HWND hwnd = |
| shell()->window()->GetRootWindow()->GetHost()->GetAcceleratedWidget(); |
| |
| ui::EventSource* dwthw = static_cast<ui::EventSource*>( |
| aura::WindowTreeHost::GetForAcceleratedWidget(hwnd)); |
| EventLogger event_logger; |
| dwthw->AddEventRewriter(&event_logger); |
| |
| ui::WindowEventTarget* target = GetWindowEventTarget(); |
| |
| { |
| target->ApplyPanGestureScroll(1, 2); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| ASSERT_TRUE(event); |
| |
| EXPECT_EQ(ui::ET_SCROLL, event->type()); |
| ui::ScrollEvent* scroll_event = event->AsScrollEvent(); |
| EXPECT_EQ(1, scroll_event->x_offset()); |
| EXPECT_EQ(2, scroll_event->y_offset()); |
| EXPECT_EQ(ui::EventMomentumPhase::NONE, scroll_event->momentum_phase()); |
| EXPECT_EQ(ui::ScrollEventPhase::kUpdate, |
| scroll_event->scroll_event_phase()); |
| } |
| |
| { |
| target->ApplyPanGestureFling(1, 2); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| ASSERT_TRUE(event); |
| |
| EXPECT_EQ(ui::ET_SCROLL, event->type()); |
| ui::ScrollEvent* scroll_event = event->AsScrollEvent(); |
| EXPECT_EQ(1, scroll_event->x_offset()); |
| EXPECT_EQ(2, scroll_event->y_offset()); |
| EXPECT_EQ(ui::EventMomentumPhase::INERTIAL_UPDATE, |
| scroll_event->momentum_phase()); |
| EXPECT_EQ(ui::ScrollEventPhase::kNone, scroll_event->scroll_event_phase()); |
| } |
| |
| { |
| target->ApplyPanGestureScrollBegin(1, 2); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_SCROLL, event->type()); |
| ui::ScrollEvent* scroll_event = event->AsScrollEvent(); |
| EXPECT_EQ(1, scroll_event->x_offset()); |
| EXPECT_EQ(2, scroll_event->y_offset()); |
| EXPECT_EQ(ui::EventMomentumPhase::NONE, scroll_event->momentum_phase()); |
| EXPECT_EQ(ui::ScrollEventPhase::kBegan, scroll_event->scroll_event_phase()); |
| } |
| |
| { |
| target->ApplyPanGestureScrollEnd(true); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_SCROLL, event->type()); |
| ui::ScrollEvent* scroll_event = event->AsScrollEvent(); |
| EXPECT_EQ(0, scroll_event->x_offset()); |
| EXPECT_EQ(0, scroll_event->y_offset()); |
| EXPECT_EQ(ui::EventMomentumPhase::BLOCKED, scroll_event->momentum_phase()); |
| EXPECT_EQ(ui::ScrollEventPhase::kEnd, scroll_event->scroll_event_phase()); |
| } |
| |
| { |
| target->ApplyPanGestureFlingBegin(); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_SCROLL, event->type()); |
| ui::ScrollEvent* scroll_event = event->AsScrollEvent(); |
| EXPECT_EQ(0, scroll_event->x_offset()); |
| EXPECT_EQ(0, scroll_event->y_offset()); |
| EXPECT_EQ(ui::EventMomentumPhase::BEGAN, scroll_event->momentum_phase()); |
| EXPECT_EQ(ui::ScrollEventPhase::kNone, scroll_event->scroll_event_phase()); |
| } |
| |
| { |
| target->ApplyPanGestureFlingEnd(); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_SCROLL, event->type()); |
| ui::ScrollEvent* scroll_event = event->AsScrollEvent(); |
| EXPECT_EQ(0, scroll_event->x_offset()); |
| EXPECT_EQ(0, scroll_event->y_offset()); |
| EXPECT_EQ(ui::EventMomentumPhase::END, scroll_event->momentum_phase()); |
| EXPECT_EQ(ui::ScrollEventPhase::kNone, scroll_event->scroll_event_phase()); |
| } |
| |
| { |
| target->ApplyPinchZoomBegin(); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_GESTURE_PINCH_BEGIN, event->type()); |
| ui::GestureEvent* gesture_event = event->AsGestureEvent(); |
| EXPECT_EQ(ui::GestureDeviceType::DEVICE_TOUCHPAD, |
| gesture_event->details().device_type()); |
| } |
| |
| { |
| target->ApplyPinchZoomScale(1.1f); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_GESTURE_PINCH_UPDATE, event->type()); |
| ui::GestureEvent* gesture_event = event->AsGestureEvent(); |
| EXPECT_EQ(ui::GestureDeviceType::DEVICE_TOUCHPAD, |
| gesture_event->details().device_type()); |
| EXPECT_EQ(1.1f, gesture_event->details().scale()); |
| } |
| |
| { |
| target->ApplyPinchZoomEnd(); |
| std::unique_ptr<ui::Event> event = event_logger.ReleaseLastEvent(); |
| ASSERT_TRUE(event); |
| EXPECT_EQ(ui::ET_GESTURE_PINCH_END, event->type()); |
| ui::GestureEvent* gesture_event = event->AsGestureEvent(); |
| EXPECT_EQ(ui::GestureDeviceType::DEVICE_TOUCHPAD, |
| gesture_event->details().device_type()); |
| } |
| |
| dwthw->RemoveEventRewriter(&event_logger); |
| } |
| |
| class PrecisionTouchpadBrowserTest : public DirectManipulationBrowserTestBase { |
| public: |
| PrecisionTouchpadBrowserTest() { |
| content_ = Microsoft::WRL::Make<MockDirectManipulationContent>(); |
| } |
| |
| void UpdateContents(float scale, float scroll_x, float scroll_y) { |
| content_->SetContentTransform(scale, scroll_x, scroll_y); |
| DirectManipulationBrowserTestBase::UpdateContents(content_.Get()); |
| } |
| |
| void UseCenterPointAsMockCursorPosition(WebContentsImpl* web_contents) { |
| SetMockCursorPositionForTesting( |
| web_contents, web_contents->GetContainerBounds().CenterPoint()); |
| } |
| |
| private: |
| Microsoft::WRL::ComPtr<MockDirectManipulationContent> content_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PrecisionTouchpadBrowserTest); |
| }; |
| |
| // Confirm that preventDefault correctly prevents pinch zoom on precision |
| // touchpad. |
| IN_PROC_BROWSER_TEST_F(PrecisionTouchpadBrowserTest, PreventDefaultPinchZoom) { |
| if (base::win::GetVersion() < base::win::Version::WIN10) |
| return; |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GURL(R"HTML(data:text/html,<!DOCTYPE html> |
| <html> |
| Hello, world |
| </html>)HTML"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderWidgetHostImpl* rwhi = web_contents->GetRenderWidgetHostWithPageFocus(); |
| |
| // Wait for a frame to be produced or else the test will sometimes try to |
| // zoom too early and be unable to, causing flakiness. |
| MainThreadFrameObserver observer( |
| web_contents->GetRenderViewHost()->GetWidget()); |
| observer.Wait(); |
| |
| UseCenterPointAsMockCursorPosition(web_contents); |
| |
| EXPECT_EQ(1, EvalJs(web_contents, "window.visualViewport.scale")); |
| |
| // Initial amount to try zooming by. |
| const int kInitialZoom = 2; |
| |
| // Used to confirm the gesture is ACK'd before checking the zoom state. The |
| // ACK result itself isn't relevant in this test. |
| auto input_msg_watcher = std::make_unique<InputMsgWatcher>( |
| web_contents->GetRenderViewHost()->GetWidget(), |
| blink::WebInputEvent::Type::kGesturePinchUpdate); |
| |
| // First, test a standard zoom. |
| UpdateContents(kInitialZoom, 0, 0); |
| input_msg_watcher->WaitForAck(); |
| EXPECT_TRUE(input_msg_watcher->HasReceivedAck()); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(kInitialZoom, EvalJs(web_contents, "window.visualViewport.scale")); |
| |
| // In order for the end event to be fired and state reset, a new sequence has |
| // to be started. This simulates lifting fingers off of the touch pad. |
| StartNewSequence(); |
| |
| // Now add the preventDefault to confirm zooming does not happen. |
| EXPECT_TRUE(ExecJs(web_contents, |
| R"(var handler = function (e) {e.preventDefault(); }; |
| document.addEventListener('wheel', handler, {passive: false}); )")); |
| RunUntilInputProcessed(rwhi); |
| |
| // Arbitrary zoom amount chosen here to make the test fail if it does zoom. |
| UpdateContents(3.5, 0, 0); |
| input_msg_watcher->WaitForAck(); |
| EXPECT_TRUE(input_msg_watcher->HasReceivedAck()); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(kInitialZoom, EvalJs(web_contents, "window.visualViewport.scale")); |
| |
| // Confirm a zoom back out to 1 works as expected. |
| StartNewSequence(); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_TRUE(ExecJs( |
| web_contents, |
| R"(document.removeEventListener('wheel', handler, {passive: false}); )")); |
| |
| const float kEndZoom = 0.5; |
| |
| UpdateContents(kEndZoom, 0, 0); |
| input_msg_watcher->WaitForAck(); |
| EXPECT_TRUE(input_msg_watcher->HasReceivedAck()); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(static_cast<int>(kInitialZoom * kEndZoom), |
| EvalJs(web_contents, "window.visualViewport.scale")); |
| } |
| |
| // Confirm that preventDefault correctly prevents scrolling on precision |
| // touchpad. |
| IN_PROC_BROWSER_TEST_F(PrecisionTouchpadBrowserTest, PreventDefaultScroll) { |
| if (base::win::GetVersion() < base::win::Version::WIN10) |
| return; |
| |
| ASSERT_TRUE(NavigateToURL(shell(), GURL(R"HTML(data:text/html,<!DOCTYPE html> |
| <html> |
| <body style='height:2000px; width:2000px;'> |
| Hello, world |
| </body> |
| </html>)HTML"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderWidgetHostImpl* rwhi = web_contents->GetRenderWidgetHostWithPageFocus(); |
| |
| // Wait for a frame to be produced or else the test will sometimes try to |
| // scroll too early and be unable to, causing flakiness. |
| MainThreadFrameObserver observer( |
| web_contents->GetRenderViewHost()->GetWidget()); |
| observer.Wait(); |
| |
| UseCenterPointAsMockCursorPosition(web_contents); |
| |
| EXPECT_EQ(0, EvalJs(web_contents, "document.documentElement.scrollLeft")); |
| EXPECT_EQ(0, EvalJs(web_contents, "document.documentElement.scrollTop")); |
| |
| // The distance to try scrolling. Note that scrolling down or right is |
| // considered the negative direction, so this value will be negated when |
| // passed to UpdateContents. |
| const int kInitialScrollDistance = 200; |
| |
| // Used to confirm the gesture is ACK'd before checking the scroll state. The |
| // ACK result itself isn't relevant in this test. |
| auto input_msg_watcher = std::make_unique<InputMsgWatcher>( |
| web_contents->GetRenderViewHost()->GetWidget(), |
| blink::WebInputEvent::Type::kMouseWheel); |
| |
| // First, test scrolling vertically |
| UpdateContents(1, 0, -kInitialScrollDistance); |
| input_msg_watcher->WaitForAck(); |
| EXPECT_TRUE(input_msg_watcher->HasReceivedAck()); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(0, EvalJs(web_contents, "document.documentElement.scrollLeft")); |
| EXPECT_EQ(kInitialScrollDistance, |
| EvalJs(web_contents, "document.documentElement.scrollTop")); |
| |
| // Then, horizontally. Note that a new sequence is not starting between these, |
| // which is why the y value remains the same. |
| UpdateContents(1, -kInitialScrollDistance, -kInitialScrollDistance); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(kInitialScrollDistance, |
| EvalJs(web_contents, "document.documentElement.scrollLeft")); |
| EXPECT_EQ(kInitialScrollDistance, |
| EvalJs(web_contents, "document.documentElement.scrollTop")); |
| |
| // In order for the end event to be fired and state reset, a new sequence has |
| // to be started. This simulates lifting fingers off of the touch pad. |
| StartNewSequence(); |
| |
| // Now add the preventDefault to confirm scrolling does not happen. |
| EXPECT_TRUE(ExecJs(web_contents, |
| R"(var handler = function (e) {e.preventDefault(); }; |
| document.addEventListener('wheel', handler, {passive: false}); )")); |
| RunUntilInputProcessed(rwhi); |
| |
| // Updating with arbitrarily chosen numbers that should make it obvious where |
| // values are coming from when this test fails. |
| UpdateContents(1, 354, 291); |
| input_msg_watcher->WaitForAck(); |
| EXPECT_TRUE(input_msg_watcher->HasReceivedAck()); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(kInitialScrollDistance, |
| EvalJs(web_contents, "document.documentElement.scrollLeft")); |
| EXPECT_EQ(kInitialScrollDistance, |
| EvalJs(web_contents, "document.documentElement.scrollTop")); |
| |
| // Confirm a scroll back towards the origin works after removing the event |
| // listener. |
| StartNewSequence(); |
| |
| EXPECT_TRUE(ExecJs( |
| web_contents, |
| R"(document.removeEventListener('wheel', handler, {passive: false}); )")); |
| |
| // Values arbitrarily chosen so to confirm that scrolling up and left works |
| // without going all the way to the origin. |
| const int kScrollXDistance = 120; |
| const int kScrollYDistance = 150; |
| UpdateContents(1, kScrollXDistance, kScrollYDistance); |
| input_msg_watcher->WaitForAck(); |
| EXPECT_TRUE(input_msg_watcher->HasReceivedAck()); |
| RunUntilInputProcessed(rwhi); |
| |
| EXPECT_EQ(kInitialScrollDistance - kScrollXDistance, |
| EvalJs(web_contents, "document.documentElement.scrollLeft")); |
| EXPECT_EQ(kInitialScrollDistance - kScrollYDistance, |
| EvalJs(web_contents, "document.documentElement.scrollTop")); |
| } |
| |
| } // namespace content |