| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/command_line.h" |
| #include "base/compiler_specific.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/render_widget_host_impl.h" |
| #include "content/browser/renderer_host/render_widget_host_view_base.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/common/input/actions_parser.h" |
| #include "content/common/input/synthetic_gesture.h" |
| #include "content/common/input/synthetic_gesture_controller.h" |
| #include "content/common/input/synthetic_gesture_params.h" |
| #include "content/common/input/synthetic_gesture_target.h" |
| #include "content/common/input/synthetic_pointer_action.h" |
| #include "content/common/input/synthetic_pointer_action_list_params.h" |
| #include "content/common/input/synthetic_smooth_scroll_gesture.h" |
| #include "content/common/input/synthetic_smooth_scroll_gesture_params.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/common/content_switches.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/public/test/hit_test_region_observer.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/shell/browser/shell.h" |
| #include "third_party/blink/public/common/input/web_input_event.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/events/blink/blink_features.h" |
| #include "ui/latency/latency_info.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "content/browser/renderer_host/input/touch_selection_controller_input_observer.h" |
| #include "content/browser/renderer_host/render_widget_host_view_android.h" |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| using blink::WebInputEvent; |
| |
| namespace { |
| |
| constexpr char kTouchActionDataURL[] = |
| "data:text/html;charset=utf-8," |
| "<!DOCTYPE html>" |
| "<meta name='viewport' content='width=device-width'/>" |
| "<style>" |
| "html, body {" |
| " margin: 0;" |
| "}" |
| ".box {" |
| " height: 100px;" |
| " width: 100px;" |
| " background-color: red;" |
| "}" |
| ".ta-none {" |
| " touch-action: none;" |
| " background-color: green;" |
| "}" |
| ".spacer {" |
| " height: 10000px;" |
| " width: 100px;" |
| " background-color: blue;" |
| "}" |
| "</style>" |
| "<div class=box></div>" |
| "<div class='box ta-none'></div>" |
| "<div class=spacer></div>" |
| "<script>" |
| " window.eventCounts = " |
| " {touchstart:0, touchmove:0, touchend: 0, touchcancel:0};" |
| " function countEvent(e) { eventCounts[e.type]++; }" |
| " for (var evt in eventCounts) { " |
| " document.addEventListener(evt, countEvent); " |
| " }" |
| " document.title='ready';" |
| "</script>"; |
| |
| constexpr char kTouchActionURLWithOverlapArea[] = |
| "data:text/html;charset=utf-8," |
| "<!DOCTYPE html>" |
| "<meta name='viewport' content='width=device-width'/>" |
| "<style>" |
| "html, body {" |
| " margin: 0;" |
| "}" |
| ".box {" |
| " box-sizing: border-box;" |
| " height: 100px;" |
| " width: 100px;" |
| " border: 2px solid blue;" |
| " position: absolute;" |
| " will-change: transform;" |
| "}" |
| ".spacer {" |
| " height: 10000px;" |
| " width: 10000px;" |
| "}" |
| ".ta-auto {" |
| " top: 52px;" |
| " left: 52px;" |
| " touch-action: auto;" |
| "}" |
| ".ta-pany {" |
| " top: 102px;" |
| " left: 2px;" |
| " touch-action: pan-y;" |
| "}" |
| ".ta-panx {" |
| " top: 2px;" |
| " left: 102px;" |
| " touch-action: pan-x;" |
| "}" |
| "</style>" |
| "<div class='box ta-auto'></div>" |
| "<div class='box ta-panx'></div>" |
| "<div class='box ta-pany'></div>" |
| "<div class=spacer></div>" |
| "<script>" |
| " document.title='ready';" |
| "</script>"; |
| |
| void GiveItSomeTime(int t) { |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(t)); |
| run_loop.Run(); |
| } |
| |
| constexpr base::TimeDelta kNoJankTime = base::Milliseconds(0); |
| constexpr base::TimeDelta kShortJankTime = base::Milliseconds(100); |
| // 1200ms is larger than both desktop / mobile_touch_ack_timeout_delay in the |
| // PassthroughTouchEventQueue, which ensures timeout to be triggered. |
| constexpr base::TimeDelta kLongJankTime = base::Milliseconds(1200); |
| } // namespace |
| |
| namespace content { |
| |
| class TouchActionBrowserTest : public ContentBrowserTest { |
| public: |
| TouchActionBrowserTest() = default; |
| |
| TouchActionBrowserTest(const TouchActionBrowserTest&) = delete; |
| TouchActionBrowserTest& operator=(const TouchActionBrowserTest&) = delete; |
| |
| ~TouchActionBrowserTest() override = default; |
| |
| RenderWidgetHostImpl* GetWidgetHost() { |
| return RenderWidgetHostImpl::From(shell() |
| ->web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetRenderViewHost() |
| ->GetWidget()); |
| } |
| |
| void OnSyntheticGestureCompleted(SyntheticGesture::Result result) { |
| EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result); |
| run_loop_->Quit(); |
| } |
| |
| protected: |
| void LoadURL(std::string_view touch_action_url) { |
| const GURL data_url(std::move(touch_action_url)); |
| EXPECT_TRUE(NavigateToURL(shell(), data_url)); |
| |
| RenderWidgetHostImpl* host = GetWidgetHost(); |
| frame_observer_ = std::make_unique<RenderFrameSubmissionObserver>( |
| host->render_frame_metadata_provider()); |
| host->GetView()->SetSize(gfx::Size(400, 400)); |
| |
| std::u16string ready_title(u"ready"); |
| TitleWatcher watcher(shell()->web_contents(), ready_title); |
| std::ignore = watcher.WaitAndGetTitle(); |
| |
| // We need to wait until hit test data is available. We use our own |
| // HitTestRegionObserver here because we have the RenderWidgetHostImpl |
| // available. |
| HitTestRegionObserver observer(host->GetFrameSinkId()); |
| observer.WaitForHitTestData(); |
| } |
| |
| // ContentBrowserTest: |
| void SetUpCommandLine(base::CommandLine* cmd) override { |
| cmd->AppendSwitchASCII(switches::kTouchEventFeatureDetection, |
| switches::kTouchEventFeatureDetectionEnabled); |
| } |
| |
| // ContentBrowserTest: |
| void PostRunTestOnMainThread() override { |
| // Delete this before the WebContents is destroyed. |
| frame_observer_.reset(); |
| ContentBrowserTest::PostRunTestOnMainThread(); |
| } |
| |
| void JankMainThread(base::TimeDelta delta) { |
| std::string script = "var end = performance.now() + "; |
| script.append(base::NumberToString(delta.InMilliseconds())); |
| script.append("; while (performance.now() < end) ; "); |
| EXPECT_TRUE(ExecJs(shell(), script)); |
| } |
| |
| double GetScrollTop() { |
| return EvalJs(shell(), "document.scrollingElement.scrollTop") |
| .ExtractDouble(); |
| } |
| |
| double GetScrollLeft() { |
| return EvalJs(shell(), "document.scrollingElement.scrollLeft") |
| .ExtractDouble(); |
| } |
| |
| bool URLLoaded() { |
| std::u16string ready_title(u"ready"); |
| TitleWatcher watcher(shell()->web_contents(), ready_title); |
| const std::u16string title = watcher.WaitAndGetTitle(); |
| return title == ready_title; |
| } |
| |
| // In this test, we first jank the main thread for 1200ms. Then we let the |
| // first finger scroll along the x-direction, on a pan-y area, for 1 second. |
| // While the first finger is still scrolling, we let the second finger |
| // touching the same area and scroll along the same direction. We purposely |
| // trigger touch ack timeout for the first finger touch. All we need to ensure |
| // is that the second finger also scrolled. |
| // TODO(bokan): This test isn't doing what's described. For one thing, the |
| // JankMainThread function will block the caller as well as the main thread |
| // so we're actually waiting 1.8s before starting the second scroll, by which |
| // point the first scroll has finished. Additionally, we can only run one |
| // synthetic gesture at a time so queueing two gestures will produce |
| // back-to-back scrolls rather than one two fingered scroll. |
| void DoTwoFingerTouchScroll( |
| bool wait_until_scrolled, |
| const gfx::Vector2d& expected_scroll_position_after_scroll) { |
| SyntheticSmoothScrollGestureParams params1; |
| params1.gesture_source_type = |
| content::mojom::GestureSourceType::kTouchInput; |
| params1.anchor = gfx::PointF(25, 125); |
| params1.distances.push_back(gfx::Vector2dF(-5, 0)); |
| params1.prevent_fling = true; |
| params1.speed_in_pixels_s = 5; |
| |
| SyntheticSmoothScrollGestureParams params2; |
| params2.gesture_source_type = |
| content::mojom::GestureSourceType::kTouchInput; |
| params2.anchor = gfx::PointF(25, 125); |
| params2.distances.push_back(gfx::Vector2dF(-50, 0)); |
| |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| |
| GetWidgetHost()->QueueSyntheticGesture( |
| std::make_unique<SyntheticSmoothScrollGesture>(params1), |
| base::DoNothing()); |
| |
| JankMainThread(kLongJankTime); |
| GiveItSomeTime(800); |
| |
| GetWidgetHost()->QueueSyntheticGesture( |
| std::make_unique<SyntheticSmoothScrollGesture>(params2), |
| base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted, |
| base::Unretained(this))); |
| |
| // Runs until we get the OnSyntheticGestureCompleted callback |
| run_loop_->Run(); |
| run_loop_.reset(); |
| |
| CheckScrollOffset(wait_until_scrolled, |
| expected_scroll_position_after_scroll); |
| } |
| |
| // Generate touch events for a synthetic scroll from |point| for |distance|. |
| void DoTouchScrollAndCheckScrollHeight( |
| const gfx::Point& point, |
| const gfx::Vector2d& distance, |
| bool wait_until_scrolled, |
| int expected_scroll_height_after_scroll, |
| const gfx::Vector2d& expected_scroll_position_after_scroll, |
| const base::TimeDelta& jank_time) { |
| EXPECT_EQ(expected_scroll_height_after_scroll, |
| EvalJs(shell(), "document.documentElement.scrollHeight")); |
| DoTouchScroll(point, distance, wait_until_scrolled, |
| expected_scroll_position_after_scroll, jank_time); |
| } |
| |
| void DoTouchScroll(const gfx::Point& point, |
| const gfx::Vector2d& distance, |
| bool wait_until_scrolled, |
| const gfx::Vector2d& expected_scroll_position_after_scroll, |
| const base::TimeDelta& jank_time) { |
| DCHECK(URLLoaded()); |
| EXPECT_EQ(0, GetScrollTop()); |
| |
| EnsureInitializedForSyntheticGestures(); |
| |
| float page_scale_factor = |
| frame_observer_->LastRenderFrameMetadata().page_scale_factor; |
| if (page_scale_factor == 0) |
| page_scale_factor = 1.0f; |
| gfx::PointF touch_point(point); |
| if (page_scale_factor != 1.0f) { |
| touch_point.set_x(touch_point.x() * page_scale_factor); |
| touch_point.set_y(touch_point.y() * page_scale_factor); |
| } |
| SyntheticSmoothScrollGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.anchor = touch_point; |
| params.distances.push_back(-distance); |
| // Set the speed to very high so that there is one GSU only. |
| // It seems that when the speed is too high, it has a race with the timeout |
| // test. |
| if (jank_time != kLongJankTime) { |
| params.speed_in_pixels_s = 1000000; |
| } |
| |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| |
| GetWidgetHost()->QueueSyntheticGesture( |
| std::make_unique<SyntheticSmoothScrollGesture>(params), |
| base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted, |
| base::Unretained(this))); |
| |
| if (jank_time > base::Milliseconds(0)) |
| JankMainThread(jank_time); |
| |
| // Runs until we get the OnSyntheticGestureCompleted callback |
| run_loop_->Run(); |
| run_loop_.reset(); |
| |
| CheckScrollOffset(wait_until_scrolled, |
| expected_scroll_position_after_scroll); |
| } |
| |
| void DoTwoFingerPan() { |
| DCHECK(URLLoaded()); |
| |
| const std::string pointer_actions_json = R"HTML( |
| [{"source": "touch", "id": 0, |
| "actions": [ |
| { "name": "pointerDown", "x": 10, "y": 125 }, |
| { "name": "pointerMove", "x": 10, "y": 155 }, |
| { "name": "pointerUp" }]}, |
| {"source": "touch", "id": 1, |
| "actions": [ |
| { "name": "pointerDown", "x": 15, "y": 125 }, |
| { "name": "pointerMove", "x": 15, "y": 155 }, |
| { "name": "pointerUp"}]}] |
| )HTML"; |
| |
| ASSERT_OK_AND_ASSIGN( |
| auto parsed_json, |
| base::JSONReader::ReadAndReturnValueWithError(pointer_actions_json)); |
| ActionsParser actions_parser(std::move(parsed_json)); |
| |
| ASSERT_TRUE(actions_parser.Parse()); |
| |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| |
| GetWidgetHost()->QueueSyntheticGesture( |
| std::make_unique<SyntheticPointerAction>( |
| actions_parser.pointer_action_params()), |
| base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted, |
| base::Unretained(this))); |
| |
| // Runs until we get the OnSyntheticGestureCompleted callback |
| run_loop_->Run(); |
| run_loop_.reset(); |
| } |
| |
| // Generate touch events for a double tap and drag zoom gesture at |
| // coordinates (50, 50). |
| void DoDoubleTapDragZoom() { |
| DCHECK(URLLoaded()); |
| |
| const std::string pointer_actions_json = R"HTML( |
| [{ |
| "source": "touch", |
| "actions": [ |
| { "name": "pointerDown", "x": 50, "y": 50 }, |
| { "name": "pointerUp" }, |
| { "name": "pause", "duration": 50 }, |
| { "name": "pointerDown", "x": 50, "y": 50 }, |
| { "name": "pointerMove", "x": 50, "y": 150 }, |
| { "name": "pointerUp" } |
| ] |
| }] |
| )HTML"; |
| |
| UNSAFE_BUFFERS(ASSERT_OK_AND_ASSIGN( |
| auto parsed_json, |
| base::JSONReader::ReadAndReturnValueWithError(pointer_actions_json))); |
| ActionsParser actions_parser(std::move(parsed_json)); |
| |
| ASSERT_TRUE(actions_parser.Parse()); |
| |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| |
| GetWidgetHost()->QueueSyntheticGesture( |
| std::make_unique<SyntheticPointerAction>( |
| actions_parser.pointer_action_params()), |
| base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted, |
| base::Unretained(this))); |
| |
| // Runs until we get the OnSyntheticGestureCompleted callback |
| run_loop_->Run(); |
| run_loop_.reset(); |
| } |
| |
| // Sends a no-op gesture to the page to ensure the SyntheticGestureController |
| // is initialized. We need to do this if the first sent gesture happens when |
| // the main thread is janked, otherwise the initialization won't happen |
| // because of the blocked main thread. |
| void EnsureInitializedForSyntheticGestures() { |
| DCHECK(URLLoaded()); |
| |
| base::RunLoop run_loop; |
| |
| GetWidgetHost()->EnsureReadyForSyntheticGestures( |
| base::BindLambdaForTesting([&]() { run_loop.Quit(); })); |
| |
| // Runs until we get the OnSyntheticGestureCompleted callback |
| run_loop.Run(); |
| } |
| |
| void CheckScrollOffset( |
| bool wait_until_scrolled, |
| const gfx::Vector2d& expected_scroll_position_after_scroll) { |
| gfx::PointF default_scroll_offset; |
| gfx::PointF root_scroll_offset = |
| frame_observer_->LastRenderFrameMetadata().root_scroll_offset.value_or( |
| default_scroll_offset); |
| |
| int scroll_top, scroll_left; |
| if (!wait_until_scrolled) { |
| scroll_top = root_scroll_offset.y(); |
| scroll_left = root_scroll_offset.x(); |
| } else { |
| // GetScrollTop() and GetScrollLeft() goes through the main thread, here |
| // we want to make sure that the compositor already scrolled before asking |
| // the main thread. |
| while (root_scroll_offset.y() < |
| expected_scroll_position_after_scroll.y() / 2 || |
| root_scroll_offset.x() < |
| expected_scroll_position_after_scroll.x() / 2) { |
| frame_observer_->WaitForMetadataChange(); |
| root_scroll_offset = |
| frame_observer_->LastRenderFrameMetadata() |
| .root_scroll_offset.value_or(default_scroll_offset); |
| } |
| // Check the scroll offset |
| scroll_top = GetScrollTop(); |
| scroll_left = GetScrollLeft(); |
| } |
| |
| // It seems that even if the compositor frame has scrolled half of the |
| // expected scroll offset, the Blink side scroll offset may not yet be |
| // updated, so here we expect it to at least have scrolled. |
| // TODO(crbug.com/40601223): this can be resolved by fixing this bug. |
| if (expected_scroll_position_after_scroll.y() > 0) |
| EXPECT_GT(scroll_top, 0); |
| if (expected_scroll_position_after_scroll.x() > 0) |
| EXPECT_GT(scroll_left, 0); |
| } |
| |
| private: |
| std::unique_ptr<RenderFrameSubmissionObserver> frame_observer_; |
| std::unique_ptr<base::RunLoop> run_loop_; |
| }; |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_DefaultAuto DISABLED_DefaultAuto |
| #else |
| #define MAYBE_DefaultAuto DefaultAuto |
| #endif |
| // |
| // Verify the test infrastructure works - we can touch-scroll the page and get a |
| // touchcancel as expected. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_DefaultAuto) { |
| LoadURL(kTouchActionDataURL); |
| |
| bool wait_until_scrolled = false; |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(50, 50), gfx::Vector2d(0, 45), |
| wait_until_scrolled, 10200, |
| gfx::Vector2d(0, 45), kNoJankTime); |
| |
| EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchstart")); |
| EXPECT_GE(EvalJs(shell(), "eventCounts.touchmove").ExtractInt(), 1); |
| EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchend")); |
| EXPECT_EQ(0, EvalJs(shell(), "eventCounts.touchcancel")); |
| } |
| |
| // Verify that touching a touch-action: none region disables scrolling and |
| // enables all touch events to be sent. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) |
| #define MAYBE_TouchActionNone DISABLED_TouchActionNone |
| #else |
| #define MAYBE_TouchActionNone TouchActionNone |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_TouchActionNone) { |
| LoadURL(kTouchActionDataURL); |
| |
| bool wait_until_scrolled = false; |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(50, 150), gfx::Vector2d(0, 45), |
| wait_until_scrolled, 10200, |
| gfx::Vector2d(0, 0), kNoJankTime); |
| |
| EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchstart")); |
| EXPECT_GE(EvalJs(shell(), "eventCounts.touchmove").ExtractInt(), 1); |
| EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchend")); |
| EXPECT_EQ(0, EvalJs(shell(), "eventCounts.touchcancel")); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanYMainThreadJanky DISABLED_PanYMainThreadJanky |
| #else |
| #define MAYBE_PanYMainThreadJanky PanYMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanYMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| bool wait_until_scrolled = false; |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(25, 125), gfx::Vector2d(0, 45), |
| wait_until_scrolled, 10000, |
| gfx::Vector2d(0, 45), kShortJankTime); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanXMainThreadJanky DISABLED_PanXMainThreadJanky |
| #else |
| #define MAYBE_PanXMainThreadJanky PanXMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanXMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| bool wait_until_scrolled = false; |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(125, 25), gfx::Vector2d(45, 0), |
| wait_until_scrolled, 10000, |
| gfx::Vector2d(45, 0), kShortJankTime); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_PanXAtYAreaWithTimeout PanXAtYAreaWithTimeout |
| #else |
| #define MAYBE_PanXAtYAreaWithTimeout DISABLED_PanXAtYAreaWithTimeout |
| #endif |
| // When touch ack timeout is triggered, the panx gesture will be allowed even |
| // though we touch the pany area. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanXAtYAreaWithTimeout) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(25, 125), gfx::Vector2d(45, 0), |
| true, 10000, gfx::Vector2d(45, 0), |
| kLongJankTime); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_TwoFingerPanXAtYAreaWithTimeout TwoFingerPanXAtYAreaWithTimeout |
| #else |
| #define MAYBE_TwoFingerPanXAtYAreaWithTimeout \ |
| DISABLED_TwoFingerPanXAtYAreaWithTimeout |
| #endif |
| // When touch ack timeout is triggered, the panx gesture will be allowed even |
| // though we touch the pany area. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, |
| MAYBE_TwoFingerPanXAtYAreaWithTimeout) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTwoFingerTouchScroll(true, gfx::Vector2d(20, 0)); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanXYMainThreadJanky DISABLED_PanXYMainThreadJanky |
| #else |
| #define MAYBE_PanXYMainThreadJanky PanXYMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanXYMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| bool wait_until_scrolled = false; |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(75, 60), gfx::Vector2d(45, 45), |
| wait_until_scrolled, 10000, |
| gfx::Vector2d(45, 45), kShortJankTime); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanXYAtXAreaMainThreadJanky DISABLED_PanXYAtXAreaMainThreadJanky |
| #else |
| #define MAYBE_PanXYAtXAreaMainThreadJanky PanXYAtXAreaMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, |
| MAYBE_PanXYAtXAreaMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(125, 25), gfx::Vector2d(45, 20), |
| true, 10000, gfx::Vector2d(45, 0), |
| kShortJankTime); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanXYAtYAreaMainThreadJanky DISABLED_PanXYAtYAreaMainThreadJanky |
| #else |
| #define MAYBE_PanXYAtYAreaMainThreadJanky PanXYAtYAreaMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, |
| MAYBE_PanXYAtYAreaMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(25, 125), gfx::Vector2d(20, 45), |
| true, 10000, gfx::Vector2d(0, 45), |
| kShortJankTime); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanXYAtAutoYOverlapAreaMainThreadJanky \ |
| DISABLED_PanXYAtAutoYOverlapAreaMainThreadJanky |
| #else |
| #define MAYBE_PanXYAtAutoYOverlapAreaMainThreadJanky \ |
| PanXYAtAutoYOverlapAreaMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, |
| MAYBE_PanXYAtAutoYOverlapAreaMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(75, 125), gfx::Vector2d(20, 45), |
| true, 10000, gfx::Vector2d(0, 45), |
| kShortJankTime); |
| } |
| |
| // TODO(crbug.com/40236573): Fix Mac failures. |
| #if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) || \ |
| defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \ |
| defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC) |
| #define MAYBE_PanXYAtAutoXOverlapAreaMainThreadJanky \ |
| DISABLED_PanXYAtAutoXOverlapAreaMainThreadJanky |
| #else |
| #define MAYBE_PanXYAtAutoXOverlapAreaMainThreadJanky \ |
| PanXYAtAutoXOverlapAreaMainThreadJanky |
| #endif |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, |
| MAYBE_PanXYAtAutoXOverlapAreaMainThreadJanky) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTouchScrollAndCheckScrollHeight(gfx::Point(125, 75), gfx::Vector2d(45, 20), |
| true, 10000, gfx::Vector2d(45, 0), |
| kShortJankTime); |
| } |
| |
| // TODO(crbug.com/41422733): Make this test work on Android. |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_TwoFingerPanYDisallowed DISABLED_TwoFingerPanYDisallowed |
| #else |
| #define MAYBE_TwoFingerPanYDisallowed TwoFingerPanYDisallowed |
| #endif |
| // Test that two finger panning is treated as pinch zoom and is disallowed when |
| // touching the pan-y area. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_TwoFingerPanYDisallowed) { |
| LoadURL(kTouchActionURLWithOverlapArea); |
| |
| DoTwoFingerPan(); |
| CheckScrollOffset(true, gfx::Vector2d(0, 0)); |
| } |
| |
| namespace { |
| |
| const std::string kDoubleTapZoomDataURL = R"HTML( |
| data:text/html,<!DOCTYPE html> |
| <meta name='viewport' content='width=device-width'/> |
| <style> |
| html, body { |
| margin: 0; |
| } |
| .spacer { height: 10000px; } |
| .touchaction { width: 75px; height: 75px; touch-action: none; } |
| </style> |
| <div class="touchaction"></div> |
| <div class=spacer></div> |
| <script> |
| document.title='ready'; |
| </script>)HTML"; |
| |
| } // namespace |
| |
| // Test that |touch-action: none| correctly blocks a double-tap and drag zoom |
| // gesture. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, BlockDoubleTapDragZoom) { |
| LoadURL(kDoubleTapZoomDataURL.c_str()); |
| |
| ASSERT_EQ(1, EvalJs(shell(), "window.visualViewport.scale")); |
| |
| DoDoubleTapDragZoom(); |
| |
| EXPECT_EQ(1, EvalJs(shell(), "window.visualViewport.scale")); |
| } |
| |
| namespace { |
| |
| constexpr char kContentEditableDataURL[] = R"HTML( |
| data:text/html,<!DOCTYPE html> |
| <meta name='viewport' content='width=device-width'/> |
| <style> |
| html, body { |
| margin: 0; |
| } |
| </style> |
| <div id='container' contenteditable style='height: 200px'> |
| 11111111111111111111111111111111 |
| </div> |
| <div class=spacer style='height: 10000px'></div> |
| <script> |
| let container = document.getElementById('container'); |
| container.focus(); |
| let textNode = container.childNodes[0]; |
| window.getSelection().setBaseAndExtent(textNode, 32, textNode, 32); |
| document.title='ready'; |
| </script>)HTML"; |
| |
| constexpr char kContentEditableHorizontalScrollableDataURL[] = R"HTML( |
| data:text/html,<!DOCTYPE html> |
| <meta name='viewport' content='width=device-width'/> |
| <style> |
| html, body { |
| margin: 0; |
| } |
| %23scroller { |
| height: 220px; |
| width: 100px; |
| white-space: nowrap; |
| overflow-x: scroll; |
| } |
| %23container { |
| height: 200px; |
| width: 500px; |
| display: inline-block; |
| } |
| </style> |
| <div id="scroller"> |
| <div id='container' contenteditable> |
| 11111111111111111111111111111111 |
| </div> |
| </div> |
| <div class=spacer style='height: 10000px'></div> |
| <script> |
| let container = document.getElementById('container'); |
| container.focus(); |
| let textNode = container.childNodes[0]; |
| window.getSelection().setBaseAndExtent(textNode, 32, textNode, 32); |
| document.title='ready'; |
| </script>)HTML"; |
| |
| constexpr char kContentEditableNonPassiveHandlerDataURL[] = R"HTML( |
| data:text/html,<!DOCTYPE html> |
| <meta name='viewport' content='width=device-width'/> |
| <style> |
| html, body { |
| margin: 0; |
| } |
| </style> |
| <div id='container' contenteditable style='height: 200px'> |
| 11111111111111111111111111111111 |
| </div> |
| <div class=spacer style='height: 10000px'></div> |
| <script> |
| let container = document.getElementById('container'); |
| container.focus(); |
| let textNode = container.childNodes[0]; |
| window.getSelection().setBaseAndExtent(textNode, 32, textNode, 32); |
| container.addEventListener("touchstart", function(event) { |
| event.preventDefault(); |
| }, {passive: false}); |
| document.title='ready'; |
| </script>)HTML"; |
| |
| constexpr char kInputTagCursorControl[] = R"HTML( |
| data:text/html,<!DOCTYPE html> |
| <meta name='viewport' content='width=device-width'/> |
| <style> |
| html, body { |
| margin: 0; |
| } |
| input { |
| height: 20px; |
| padding: 0px; |
| margin: 0px; |
| border: 0px; |
| } |
| </style> |
| <input type="text" id="container" value="11111111111111111111111111111111" |
| size=%d> |
| <div class=spacer style='height: 10000px'></div> |
| <script> |
| let container = document.getElementById('container'); |
| container.focus(); |
| container.setSelectionRange(32, 32); |
| document.title='ready'; |
| </script>)HTML"; |
| |
| } // namespace |
| |
| class TouchActionBrowserTestEnableCursorControl |
| : public TouchActionBrowserTest { |
| public: |
| TouchActionBrowserTestEnableCursorControl() { |
| feature_list_.InitWithFeatures({::features::kSwipeToMoveCursor}, {}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Perform a horizontal swipe over an editable element from right to left. |
| // Ensure the swipe is interpreted as a cursor control movement, rather than a |
| // scroll, and changes the selection. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl, |
| BasicCursorControl) { |
| if (!::features::IsSwipeToMoveCursorEnabled()) |
| return; |
| LoadURL(kContentEditableDataURL); |
| |
| EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().anchorOffset")); |
| EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().focusOffset")); |
| |
| DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0), |
| /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0), |
| kNoJankTime); |
| |
| const int anchor_offset = |
| EvalJs(shell(), "window.getSelection().anchorOffset").ExtractInt(); |
| |
| EXPECT_EQ(anchor_offset, |
| EvalJs(shell(), "window.getSelection().focusOffset")); |
| EXPECT_GT(32, anchor_offset); |
| } |
| |
| // Perform a horizontal swipe over an editable element from right to left (the |
| // element shift to left), the element is inside of a horizontal scroller. |
| // Ensure the swipe is interpreted as a normal scroll, selection should not be |
| // changed and scroll should happen. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl, |
| NoCursorControlForHorizontalScrollable) { |
| if (!::features::IsSwipeToMoveCursorEnabled()) |
| return; |
| LoadURL(kContentEditableHorizontalScrollableDataURL); |
| |
| EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().anchorOffset")); |
| EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().focusOffset")); |
| |
| DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0), |
| /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0), |
| kNoJankTime); |
| |
| const int anchor_offset = |
| EvalJs(shell(), "window.getSelection().anchorOffset").ExtractInt(); |
| |
| EXPECT_EQ(anchor_offset, |
| EvalJs(shell(), "window.getSelection().focusOffset")); |
| EXPECT_EQ(32, anchor_offset); |
| EXPECT_LT(0.f, |
| EvalJs(shell(), "document.getElementById('scroller').scrollLeft") |
| .ExtractDouble()); |
| } |
| |
| // Perform a horizontal swipe over an editable element from right to left |
| // Ensure the swipe is not triggering cursor control. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl, |
| NoCursorControlForNonPassiveLisenter) { |
| if (!::features::IsSwipeToMoveCursorEnabled()) |
| return; |
| LoadURL(kContentEditableNonPassiveHandlerDataURL); |
| |
| EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().anchorOffset")); |
| EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().focusOffset")); |
| |
| DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0), |
| /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0), |
| kNoJankTime); |
| |
| const int anchor_offset = |
| EvalJs(shell(), "window.getSelection().anchorOffset").ExtractInt(); |
| |
| EXPECT_EQ(anchor_offset, |
| EvalJs(shell(), "window.getSelection().focusOffset")); |
| EXPECT_EQ(32, anchor_offset); |
| } |
| |
| // Perform a horizontal swipe over an input element from right to left. |
| // Ensure the swipe is interpreted as a cursor control movement, rather than a |
| // scroll, and changes the selection. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl, |
| CursorControlOnInput) { |
| if (!::features::IsSwipeToMoveCursorEnabled()) |
| return; |
| // input size larger than the text size, not horizontally scrollable. |
| LoadURL(base::StringPrintf(kInputTagCursorControl, 40).c_str()); |
| |
| EXPECT_EQ(32, EvalJs(shell(), "container.selectionStart")); |
| EXPECT_EQ(32, EvalJs(shell(), "container.selectionEnd")); |
| |
| DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0), |
| /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0), |
| kNoJankTime); |
| |
| const int selection_start = |
| EvalJs(shell(), "container.selectionStart").ExtractInt(); |
| |
| EXPECT_EQ(selection_start, EvalJs(shell(), "container.selectionEnd")); |
| EXPECT_GT(32, selection_start); |
| } |
| |
| // Perform a horizontal swipe over an horizontal scrollable input element from |
| // right to left. Ensure the swipe is doing scrolling other than cursor control. |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl, |
| NoCursorControlOnHorizontalScrollableInput) { |
| if (!::features::IsSwipeToMoveCursorEnabled()) |
| return; |
| // Make the input size smaller than the text size, so it horizontally |
| // scrollable. |
| LoadURL(base::StringPrintf(kInputTagCursorControl, 20).c_str()); |
| |
| EXPECT_EQ(32, EvalJs(shell(), "container.selectionStart")); |
| EXPECT_EQ(32, EvalJs(shell(), "container.selectionEnd")); |
| |
| DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0), |
| /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0), |
| kNoJankTime); |
| |
| const int selection_start = |
| EvalJs(shell(), "container.selectionStart").ExtractInt(); |
| |
| EXPECT_EQ(selection_start, EvalJs(shell(), "container.selectionEnd")); |
| EXPECT_EQ(32, selection_start); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| class ScrollBeginObserver : public RenderWidgetHost::InputEventObserver { |
| public: |
| ScrollBeginObserver(RenderWidgetHost& rwh, |
| RenderWidgetHostViewAndroid& rwhv_android, |
| base::OnceClosure quit_closure) |
| : rwh_(rwh), |
| rwhv_android_(rwhv_android), |
| quit_closure_(std::move(quit_closure)) { |
| rwh_->AddInputEventObserver(this); |
| } |
| |
| ~ScrollBeginObserver() override { rwh_->RemoveInputEventObserver(this); } |
| |
| void OnInputEvent(const RenderWidgetHost&, |
| const blink::WebInputEvent& event) override { |
| if (event.GetType() != blink::WebInputEvent::Type::kGestureScrollBegin) { |
| return; |
| } |
| // Insertion handles were active due to first scroll and should become |
| // inactive after view is removed from hierarchy. |
| EXPECT_EQ(rwhv_android_->touch_selection_controller()->active_status(), |
| ui::TouchSelectionController::ActiveStatus::INSERTION_ACTIVE); |
| ui::ViewAndroid* view_android = rwhv_android_->GetNativeView(); |
| view_android->RemoveFromParent(); |
| EXPECT_EQ(rwhv_android_->touch_selection_controller()->active_status(), |
| ui::TouchSelectionController::ActiveStatus::INACTIVE); |
| } |
| |
| void OnInputEventAck(const RenderWidgetHost&, |
| blink::mojom::InputEventResultSource, |
| blink::mojom::InputEventResultState, |
| const blink::WebInputEvent& event) override { |
| if (event.GetType() != blink::WebInputEvent::Type::kGestureScrollBegin) { |
| return; |
| } |
| // Post a task so that it's guaranteed |
| // TouchSelectionControllerInputObserver would have also processed this |
| // problematic ack which comes after view having already been removed from |
| // hierarchy. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(quit_closure_)); |
| } |
| |
| private: |
| raw_ref<RenderWidgetHost> rwh_; |
| raw_ref<RenderWidgetHostViewAndroid> rwhv_android_; |
| base::OnceClosure quit_closure_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl, |
| CursorControlDetachViewMidScroll) { |
| if (!::features::IsSwipeToMoveCursorEnabled()) { |
| return; |
| } |
| |
| LoadURL(base::StringPrintf(kInputTagCursorControl, 40).c_str()); |
| |
| EXPECT_EQ(32, EvalJs(shell(), "container.selectionStart")); |
| EXPECT_EQ(32, EvalJs(shell(), "container.selectionEnd")); |
| |
| // Do a scroll over input field to activate insertion handle. |
| DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0), |
| /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0), |
| kNoJankTime); |
| |
| auto* rwhv_android = |
| static_cast<RenderWidgetHostViewAndroid*>(GetWidgetHost()->GetView()); |
| ASSERT_TRUE(rwhv_android); |
| EXPECT_EQ(rwhv_android->touch_selection_controller()->active_status(), |
| ui::TouchSelectionController::ActiveStatus::INSERTION_ACTIVE); |
| |
| base::RunLoop run_loop; |
| ScrollBeginObserver observer(*GetWidgetHost(), *rwhv_android, |
| run_loop.QuitClosure()); |
| |
| float page_scale_factor = GetWidgetHost() |
| ->render_frame_metadata_provider() |
| ->LastRenderFrameMetadata() |
| .page_scale_factor; |
| if (page_scale_factor == 0) { |
| page_scale_factor = 1.0f; |
| } |
| gfx::PointF touch_point(85, 5); |
| touch_point.Scale(page_scale_factor); |
| |
| SyntheticSmoothScrollGestureParams params; |
| params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput; |
| params.anchor = touch_point; |
| params.distances.emplace_back(-80, 0); |
| |
| GetWidgetHost()->QueueSyntheticGesture( |
| std::make_unique<SyntheticSmoothScrollGesture>(params), |
| base::DoNothing()); |
| |
| // We expect the run loop be exited when ScrollBeginObserver processes |
| // ScrollBegin ack. |
| run_loop.Run(); |
| EXPECT_TRUE(rwhv_android->GetTouchSelectionControllerInputObserver() |
| ->HasSeenScrollBeginAckForTesting()); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| } // namespace content |