| /* |
| * Copyright (C) 2012 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND |
| * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR |
| * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "cc/base/features.h" |
| #include "cc/input/main_thread_scrolling_reason.h" |
| #include "cc/layers/scrollbar_layer_base.h" |
| #include "cc/trees/compositor_commit_data.h" |
| #include "cc/trees/property_tree.h" |
| #include "cc/trees/scroll_node.h" |
| #include "cc/trees/sticky_position_constraint.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h" |
| #include "third_party/blink/public/platform/web_cache.h" |
| #include "third_party/blink/public/platform/web_url_loader_mock_factory.h" |
| #include "third_party/blink/public/web/web_settings.h" |
| #include "third_party/blink/public/web/web_view_client.h" |
| #include "third_party/blink/renderer/core/css/css_style_sheet.h" |
| #include "third_party/blink/renderer/core/css/style_sheet_list.h" |
| #include "third_party/blink/renderer/core/dom/events/add_event_listener_options_resolved.h" |
| #include "third_party/blink/renderer/core/dom/events/native_event_listener.h" |
| #include "third_party/blink/renderer/core/exported/web_plugin_container_impl.h" |
| #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/visual_viewport.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/html/html_iframe_element.h" |
| #include "third_party/blink/renderer/core/html/html_object_element.h" |
| #include "third_party/blink/renderer/core/layout/layout_embedded_content.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/page/scrolling/scrolling_coordinator.h" |
| #include "third_party/blink/renderer/core/page/scrolling/scrolling_coordinator_context.h" |
| #include "third_party/blink/renderer/core/paint/compositing/composited_layer_mapping.h" |
| #include "third_party/blink/renderer/core/paint/compositing/paint_layer_compositor.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" |
| #include "third_party/blink/renderer/core/testing/sim/sim_request.h" |
| #include "third_party/blink/renderer/core/testing/sim/sim_test.h" |
| #include "third_party/blink/renderer/platform/geometry/int_point.h" |
| #include "third_party/blink/renderer/platform/geometry/int_rect.h" |
| #include "third_party/blink/renderer/platform/graphics/gpu/shared_gpu_context.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_layer.h" |
| #include "third_party/blink/renderer/platform/graphics/test/fake_gles2_interface.h" |
| #include "third_party/blink/renderer/platform/graphics/test/fake_web_graphics_context_3d_provider.h" |
| #include "third_party/blink/renderer/platform/graphics/touch_action.h" |
| #include "third_party/blink/renderer/platform/testing/find_cc_layer.h" |
| #include "third_party/blink/renderer/platform/testing/histogram_tester.h" |
| #include "third_party/blink/renderer/platform/testing/paint_test_configurations.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" |
| #include "ui/base/ui_base_features.h" |
| |
| namespace blink { |
| |
| class ScrollingTest : public testing::Test, public PaintTestConfigurations { |
| public: |
| ScrollingTest() : base_url_("http://www.test.com/") { |
| helper_.Initialize(nullptr, nullptr, &ConfigureSettings); |
| GetWebView()->MainFrameViewWidget()->Resize(gfx::Size(320, 240)); |
| GetWebView()->MainFrameViewWidget()->UpdateAllLifecyclePhases( |
| DocumentUpdateReason::kTest); |
| } |
| |
| ~ScrollingTest() override { |
| url_test_helpers::UnregisterAllURLsAndClearMemoryCache(); |
| } |
| |
| void NavigateTo(const std::string& url) { |
| frame_test_helpers::LoadFrame(GetWebView()->MainFrameImpl(), url); |
| } |
| |
| void LoadHTML(const std::string& html) { |
| frame_test_helpers::LoadHTMLString(GetWebView()->MainFrameImpl(), html, |
| url_test_helpers::ToKURL("about:blank")); |
| } |
| |
| void ForceFullCompositingUpdate() { |
| GetWebView()->MainFrameViewWidget()->UpdateAllLifecyclePhases( |
| DocumentUpdateReason::kTest); |
| } |
| |
| void RegisterMockedHttpURLLoad(const std::string& file_name) { |
| // TODO(crbug.com/751425): We should use the mock functionality |
| // via |helper_|. |
| url_test_helpers::RegisterMockedURLLoadFromBase( |
| WebString::FromUTF8(base_url_), test::CoreTestDataPath(), |
| WebString::FromUTF8(file_name)); |
| } |
| |
| WebViewImpl* GetWebView() const { return helper_.GetWebView(); } |
| LocalFrame* GetFrame() const { return helper_.LocalMainFrame()->GetFrame(); } |
| |
| frame_test_helpers::TestWebFrameWidget* GetMainFrameWidget() const { |
| return helper_.GetMainFrameWidget(); |
| } |
| |
| PaintLayerScrollableArea* ScrollableAreaByDOMElementId( |
| const char* id_value) const { |
| return GetFrame() |
| ->GetDocument() |
| ->getElementById(id_value) |
| ->GetLayoutBoxForScrolling() |
| ->GetScrollableArea(); |
| } |
| |
| void LoadAhem() { helper_.LoadAhem(); } |
| |
| const cc::ScrollNode* ScrollNodeForScrollableArea( |
| const ScrollableArea* scrollable_area) const { |
| if (!scrollable_area) |
| return nullptr; |
| const auto* property_trees = |
| RootCcLayer()->layer_tree_host()->property_trees(); |
| return property_trees->scroll_tree.Node( |
| property_trees->element_id_to_scroll_node_index.at( |
| scrollable_area->GetScrollElementId())); |
| } |
| |
| const cc::ScrollNode* ScrollNodeByDOMElementId(const char* dom_id) const { |
| return ScrollNodeForScrollableArea(ScrollableAreaByDOMElementId(dom_id)); |
| } |
| |
| gfx::ScrollOffset CurrentScrollOffset(cc::ElementId element_id) const { |
| return RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.current_scroll_offset(element_id); |
| } |
| |
| gfx::ScrollOffset CurrentScrollOffset( |
| const cc::ScrollNode* scroll_node) const { |
| return CurrentScrollOffset(scroll_node->element_id); |
| } |
| |
| const cc::ScrollbarLayerBase* ScrollbarLayerForScrollNode( |
| const cc::ScrollNode* scroll_node, |
| cc::ScrollbarOrientation orientation) const { |
| return blink::ScrollbarLayerForScrollNode(RootCcLayer(), scroll_node, |
| orientation); |
| } |
| |
| const cc::Layer* RootCcLayer() const { |
| return GetFrame()->View()->RootCcLayer(); |
| } |
| |
| cc::LayerTreeHost* LayerTreeHost() const { |
| return helper_.GetLayerTreeHost(); |
| } |
| |
| const cc::Layer* FrameScrollingContentsLayer(const LocalFrame& frame) const { |
| return ScrollingContentsCcLayerByScrollElementId( |
| RootCcLayer(), frame.View()->LayoutViewport()->GetScrollElementId()); |
| } |
| |
| const cc::Layer* MainFrameScrollingContentsLayer() const { |
| return FrameScrollingContentsLayer(*GetFrame()); |
| } |
| |
| const cc::Layer* LayerByDOMElementId(const char* dom_id) const { |
| return CcLayersByDOMElementId(RootCcLayer(), dom_id)[0]; |
| } |
| |
| const cc::Layer* ScrollingContentsLayerByDOMElementId( |
| const char* element_id) const { |
| const auto* scrollable_area = ScrollableAreaByDOMElementId(element_id); |
| return ScrollingContentsCcLayerByScrollElementId( |
| RootCcLayer(), scrollable_area->GetScrollElementId()); |
| } |
| |
| protected: |
| std::string base_url_; |
| |
| private: |
| static void ConfigureSettings(WebSettings* settings) { |
| settings->SetPreferCompositingToLCDTextEnabled(true); |
| } |
| |
| frame_test_helpers::WebViewHelper helper_; |
| }; |
| |
| INSTANTIATE_PAINT_TEST_SUITE_P(ScrollingTest); |
| |
| TEST_P(ScrollingTest, fastScrollingByDefault) { |
| GetWebView()->MainFrameViewWidget()->Resize(gfx::Size(800, 600)); |
| LoadHTML("<div id='spacer' style='height: 1000px'></div>"); |
| ForceFullCompositingUpdate(); |
| |
| // Make sure the scrolling coordinator is active. |
| LocalFrameView* frame_view = GetFrame()->View(); |
| Page* page = GetFrame()->GetPage(); |
| ASSERT_TRUE(page->GetScrollingCoordinator()); |
| |
| // Fast scrolling should be enabled by default. |
| const auto* outer_scroll_node = |
| ScrollNodeForScrollableArea(frame_view->LayoutViewport()); |
| ASSERT_TRUE(outer_scroll_node); |
| EXPECT_FALSE(outer_scroll_node->main_thread_scrolling_reasons); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kNone, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kTouchStartOrMove)); |
| ASSERT_EQ(cc::EventListenerProperties::kNone, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kMouseWheel)); |
| |
| const auto* inner_scroll_node = |
| ScrollNodeForScrollableArea(&page->GetVisualViewport()); |
| ASSERT_TRUE(inner_scroll_node); |
| EXPECT_FALSE(inner_scroll_node->main_thread_scrolling_reasons); |
| } |
| |
| TEST_P(ScrollingTest, fastFractionalScrollingDiv) { |
| ScopedFractionalScrollOffsetsForTest fractional_scroll_offsets(true); |
| |
| RegisterMockedHttpURLLoad("fractional-scroll-div.html"); |
| NavigateTo(base_url_ + "fractional-scroll-div.html"); |
| ForceFullCompositingUpdate(); |
| |
| Document* document = GetFrame()->GetDocument(); |
| Element* scrollable_element = document->getElementById("scroller"); |
| DCHECK(scrollable_element); |
| |
| scrollable_element->setScrollTop(1.0); |
| scrollable_element->setScrollLeft(1.0); |
| ForceFullCompositingUpdate(); |
| |
| // Make sure the fractional scroll offset change 1.0 -> 1.2 gets propagated |
| // to compositor. |
| scrollable_element->setScrollTop(1.2); |
| scrollable_element->setScrollLeft(1.2); |
| ForceFullCompositingUpdate(); |
| |
| const auto* scroll_node = ScrollNodeByDOMElementId("scroller"); |
| ASSERT_TRUE(scroll_node); |
| ASSERT_NEAR(1.2f, CurrentScrollOffset(scroll_node).x(), 0.01f); |
| ASSERT_NEAR(1.2f, CurrentScrollOffset(scroll_node).y(), 0.01f); |
| } |
| |
| TEST_P(ScrollingTest, fastScrollingForFixedPosition) { |
| RegisterMockedHttpURLLoad("fixed-position.html"); |
| NavigateTo(base_url_ + "fixed-position.html"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* scroll_node = |
| ScrollNodeForScrollableArea(GetFrame()->View()->LayoutViewport()); |
| ASSERT_TRUE(scroll_node); |
| EXPECT_FALSE(scroll_node->main_thread_scrolling_reasons); |
| } |
| |
| // Sticky constraints are stored on transform property tree nodes. |
| static cc::StickyPositionConstraint GetStickyConstraint(Element* element) { |
| const auto* properties = |
| element->GetLayoutObject()->FirstFragment().PaintProperties(); |
| DCHECK(properties); |
| return *properties->StickyTranslation()->GetStickyConstraint(); |
| } |
| |
| TEST_P(ScrollingTest, fastScrollingForStickyPosition) { |
| RegisterMockedHttpURLLoad("sticky-position.html"); |
| NavigateTo(base_url_ + "sticky-position.html"); |
| ForceFullCompositingUpdate(); |
| |
| // Sticky position should not fall back to main thread scrolling. |
| const auto* scroll_node = |
| ScrollNodeForScrollableArea(GetFrame()->View()->LayoutViewport()); |
| ASSERT_TRUE(scroll_node); |
| EXPECT_FALSE(scroll_node->main_thread_scrolling_reasons); |
| |
| Document* document = GetFrame()->GetDocument(); |
| { |
| Element* element = document->getElementById("div-tl"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(constraint.is_anchored_top && constraint.is_anchored_left && |
| !constraint.is_anchored_right && |
| !constraint.is_anchored_bottom); |
| EXPECT_EQ(1.f, constraint.top_offset); |
| EXPECT_EQ(1.f, constraint.left_offset); |
| EXPECT_EQ(gfx::RectF(100, 100, 10, 10), |
| constraint.scroll_container_relative_sticky_box_rect); |
| EXPECT_EQ(gfx::RectF(100, 100, 200, 200), |
| constraint.scroll_container_relative_containing_block_rect); |
| } |
| { |
| Element* element = document->getElementById("div-tr"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(constraint.is_anchored_top && !constraint.is_anchored_left && |
| constraint.is_anchored_right && !constraint.is_anchored_bottom); |
| } |
| { |
| Element* element = document->getElementById("div-bl"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(!constraint.is_anchored_top && constraint.is_anchored_left && |
| !constraint.is_anchored_right && constraint.is_anchored_bottom); |
| } |
| { |
| Element* element = document->getElementById("div-br"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(!constraint.is_anchored_top && !constraint.is_anchored_left && |
| constraint.is_anchored_right && constraint.is_anchored_bottom); |
| } |
| { |
| Element* element = document->getElementById("span-tl"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(constraint.is_anchored_top && constraint.is_anchored_left && |
| !constraint.is_anchored_right && |
| !constraint.is_anchored_bottom); |
| } |
| { |
| Element* element = document->getElementById("span-tlbr"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(constraint.is_anchored_top && constraint.is_anchored_left && |
| constraint.is_anchored_right && constraint.is_anchored_bottom); |
| EXPECT_EQ(1.f, constraint.top_offset); |
| EXPECT_EQ(1.f, constraint.left_offset); |
| EXPECT_EQ(1.f, constraint.right_offset); |
| EXPECT_EQ(1.f, constraint.bottom_offset); |
| } |
| { |
| Element* element = document->getElementById("composited-top"); |
| auto constraint = GetStickyConstraint(element); |
| EXPECT_TRUE(constraint.is_anchored_top); |
| EXPECT_EQ(gfx::RectF(100, 110, 10, 10), |
| constraint.scroll_container_relative_sticky_box_rect); |
| EXPECT_EQ(gfx::RectF(100, 100, 200, 200), |
| constraint.scroll_container_relative_containing_block_rect); |
| } |
| } |
| |
| TEST_P(ScrollingTest, elementPointerEventHandler) { |
| LoadHTML(R"HTML( |
| <div id="pointer" style="width: 100px; height: 100px;"></div> |
| <script> |
| pointer.addEventListener('pointerdown', function(event) { |
| }, {blocking: false} ); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| // Pointer event handlers should not generate blocking touch action regions. |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, touchEventHandler) { |
| RegisterMockedHttpURLLoad("touch-event-handler.html"); |
| NavigateTo(base_url_ + "touch-event-handler.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kBlocking, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kTouchStartOrMove)); |
| } |
| |
| TEST_P(ScrollingTest, elementBlockingTouchEventHandler) { |
| LoadHTML(R"HTML( |
| <div id="blocking" style="width: 100px; height: 100px;"></div> |
| <script> |
| blocking.addEventListener('touchstart', function(event) { |
| }, {passive: false} ); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, touchEventHandlerPassive) { |
| RegisterMockedHttpURLLoad("touch-event-handler-passive.html"); |
| NavigateTo(base_url_ + "touch-event-handler-passive.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kPassive, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kTouchStartOrMove)); |
| } |
| |
| TEST_P(ScrollingTest, elementTouchEventHandlerPassive) { |
| LoadHTML(R"HTML( |
| <div id="passive" style="width: 100px; height: 100px;"></div> |
| <script> |
| passive.addEventListener('touchstart', function(event) { |
| }, {passive: true} ); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| // Passive event handlers should not generate blocking touch action regions. |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, TouchActionRectsOnImage) { |
| LoadHTML(R"HTML( |
| <img id="image" style="width: 100px; height: 100px; touch-action: none;"> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, touchEventHandlerBoth) { |
| RegisterMockedHttpURLLoad("touch-event-handler-both.html"); |
| NavigateTo(base_url_ + "touch-event-handler-both.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kBlockingAndPassive, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kTouchStartOrMove)); |
| } |
| |
| TEST_P(ScrollingTest, wheelEventHandler) { |
| RegisterMockedHttpURLLoad("wheel-event-handler.html"); |
| NavigateTo(base_url_ + "wheel-event-handler.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kBlocking, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kMouseWheel)); |
| } |
| |
| TEST_P(ScrollingTest, wheelEventHandlerPassive) { |
| RegisterMockedHttpURLLoad("wheel-event-handler-passive.html"); |
| NavigateTo(base_url_ + "wheel-event-handler-passive.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kPassive, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kMouseWheel)); |
| } |
| |
| TEST_P(ScrollingTest, wheelEventHandlerBoth) { |
| RegisterMockedHttpURLLoad("wheel-event-handler-both.html"); |
| NavigateTo(base_url_ + "wheel-event-handler-both.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_EQ(cc::EventListenerProperties::kBlockingAndPassive, |
| LayerTreeHost()->event_listener_properties( |
| cc::EventListenerClass::kMouseWheel)); |
| } |
| |
| TEST_P(ScrollingTest, scrollEventHandler) { |
| RegisterMockedHttpURLLoad("scroll-event-handler.html"); |
| NavigateTo(base_url_ + "scroll-event-handler.html"); |
| ForceFullCompositingUpdate(); |
| |
| ASSERT_TRUE(GetMainFrameWidget()->HaveScrollEventHandlers()); |
| } |
| |
| TEST_P(ScrollingTest, updateEventHandlersDuringTeardown) { |
| RegisterMockedHttpURLLoad("scroll-event-handler-window.html"); |
| NavigateTo(base_url_ + "scroll-event-handler-window.html"); |
| ForceFullCompositingUpdate(); |
| |
| // Simulate detaching the document from its DOM window. This should not |
| // cause a crash when the WebViewImpl is closed by the test runner. |
| GetFrame()->GetDocument()->Shutdown(); |
| } |
| |
| TEST_P(ScrollingTest, clippedBodyTest) { |
| RegisterMockedHttpURLLoad("clipped-body.html"); |
| NavigateTo(base_url_ + "clipped-body.html"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* root_scroll_layer = MainFrameScrollingContentsLayer(); |
| EXPECT_TRUE(root_scroll_layer->non_fast_scrollable_region().IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, touchAction) { |
| RegisterMockedHttpURLLoad("touch-action.html"); |
| NavigateTo(base_url_ + "touch-action.html"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanX | TouchAction::kPanDown | |
| TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 1000, 1000)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionRegions) { |
| RegisterMockedHttpURLLoad("touch-action-regions.html"); |
| NavigateTo(base_url_ + "touch-action-regions.html"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanDown | TouchAction::kPanX | |
| TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 100, 100)); |
| |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanDown | TouchAction::kPanRight | |
| TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 50, 50)); |
| |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanDown); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 100, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionNesting) { |
| LoadHTML(R"HTML( |
| <style> |
| #scrollable { |
| width: 200px; |
| height: 200px; |
| background: blue; |
| overflow: scroll; |
| } |
| #touchaction { |
| touch-action: pan-x; |
| width: 100px; |
| height: 100px; |
| margin: 5px; |
| } |
| #child { |
| width: 150px; |
| height: 50px; |
| } |
| </style> |
| <div id="scrollable"> |
| <div id="touchaction"> |
| <div id="child"></div> |
| </div> |
| <div id="forcescroll" style="width: 1000px; height: 1000px;"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanX | TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.GetRegionComplexity(), 2); |
| EXPECT_EQ(region.bounds(), gfx::Rect(5, 5, 150, 100)); |
| } |
| |
| TEST_P(ScrollingTest, nestedTouchActionInvalidation) { |
| LoadHTML(R"HTML( |
| <style> |
| #scrollable { |
| width: 200px; |
| height: 200px; |
| background: blue; |
| overflow: scroll; |
| } |
| #touchaction { |
| touch-action: pan-x; |
| width: 100px; |
| height: 100px; |
| margin: 5px; |
| } |
| #child { |
| width: 150px; |
| height: 50px; |
| } |
| </style> |
| <div id="scrollable"> |
| <div id="touchaction"> |
| <div id="child"></div> |
| </div> |
| <div id="forcescroll" style="width: 1000px; height: 1000px;"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanX | TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.GetRegionComplexity(), 2); |
| EXPECT_EQ(region.bounds(), gfx::Rect(5, 5, 150, 100)); |
| |
| auto* scrollable = GetFrame()->GetDocument()->getElementById("scrollable"); |
| scrollable->setAttribute("style", "touch-action: none", ASSERT_NO_EXCEPTION); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanX | TouchAction::kInternalPanXScrolls); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| // Similar to nestedTouchActionInvalidation but tests that an ancestor with |
| // touch-action: pan-x and a descendant with touch-action: pan-y results in a |
| // touch-action rect of none for the descendant. |
| TEST_P(ScrollingTest, nestedTouchActionChangesUnion) { |
| LoadHTML(R"HTML( |
| <style> |
| #ancestor { |
| width: 100px; |
| height: 100px; |
| } |
| #child { |
| touch-action: pan-x; |
| width: 150px; |
| height: 50px; |
| } |
| </style> |
| <div id="ancestor"> |
| <div id="child"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanX | TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 150, 50)); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| Element* ancestor = GetFrame()->GetDocument()->getElementById("ancestor"); |
| ancestor->setAttribute(html_names::kStyleAttr, "touch-action: pan-y"); |
| ForceFullCompositingUpdate(); |
| |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanY); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 100, 100)); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanX | TouchAction::kInternalPanXScrolls); |
| EXPECT_TRUE(region.IsEmpty()); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 150, 50)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionEditableElement) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitWithFeatures({::features::kSwipeToMoveCursor}, {}); |
| if (!::features::IsSwipeToMoveCursorEnabled()) |
| return; |
| // Long text that will overflow in y-direction. |
| LoadHTML(R"HTML( |
| <style> |
| #touchaction { |
| touch-action: manipulation; |
| width: 100px; |
| height: 50px; |
| overflow: scroll; |
| } |
| </style> |
| <div id="touchaction" contenteditable> |
| <div id="child"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kManipulation); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 100, 50)); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| // Make touchaction scrollable by making child overflow. |
| Element* child = GetFrame()->GetDocument()->getElementById("child"); |
| child->setAttribute("style", "width: 1000px; height: 100px;", |
| ASSERT_NO_EXCEPTION); |
| ForceFullCompositingUpdate(); |
| |
| cc_layer = ScrollingContentsLayerByDOMElementId("touchaction"); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kManipulation | TouchAction::kInternalPanXScrolls); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 1000, 100)); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| // Box shadow is not hit testable and should not be included in touch action. |
| TEST_P(ScrollingTest, touchActionExcludesBoxShadow) { |
| LoadHTML(R"HTML( |
| <style> |
| #shadow { |
| width: 100px; |
| height: 100px; |
| touch-action: none; |
| box-shadow: 10px 5px 5px red; |
| } |
| </style> |
| <div id="shadow"></div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionOnInline) { |
| RegisterMockedHttpURLLoad("touch-action-on-inline.html"); |
| NavigateTo(base_url_ + "touch-action-on-inline.html"); |
| LoadAhem(); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 120, 50)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionOnText) { |
| RegisterMockedHttpURLLoad("touch-action-on-text.html"); |
| NavigateTo(base_url_ + "touch-action-on-text.html"); |
| LoadAhem(); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 160, 30)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionWithVerticalRLWritingMode) { |
| RegisterMockedHttpURLLoad("touch-action-with-vertical-rl-writing-mode.html"); |
| NavigateTo(base_url_ + "touch-action-with-vertical-rl-writing-mode.html"); |
| LoadAhem(); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(292, 8, 20, 80)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionBlockingHandler) { |
| RegisterMockedHttpURLLoad("touch-action-blocking-handler.html"); |
| NavigateTo(base_url_ + "touch-action-blocking-handler.html"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 100, 100)); |
| |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanY); |
| EXPECT_EQ(region.GetRegionComplexity(), 2); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 1000, 1000)); |
| } |
| |
| TEST_P(ScrollingTest, touchActionOnScrollingElement) { |
| LoadHTML(R"HTML( |
| <style> |
| #scrollable { |
| width: 100px; |
| height: 100px; |
| overflow: scroll; |
| touch-action: pan-y; |
| } |
| #child { |
| width: 50px; |
| height: 150px; |
| } |
| </style> |
| <div id="scrollable"> |
| <div id="child"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| // The outer layer (not scrollable) will be fully marked as pan-y (100x100) |
| // and the scrollable layer will only have the contents marked as pan-y |
| // (50x150). |
| const auto* scrolling_contents_layer = |
| ScrollingContentsLayerByDOMElementId("scrollable"); |
| cc::Region region = |
| scrolling_contents_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanY); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 50, 150)); |
| |
| const auto* container_layer = |
| RuntimeEnabledFeatures::CompositeAfterPaintEnabled() |
| ? MainFrameScrollingContentsLayer() |
| : LayerByDOMElementId("scrollable"); |
| region = container_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kPanY); |
| EXPECT_EQ(region.bounds(), |
| RuntimeEnabledFeatures::CompositeAfterPaintEnabled() |
| ? gfx::Rect(8, 8, 100, 100) |
| : gfx::Rect(0, 0, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, IframeWindowTouchHandler) { |
| LoadHTML(R"HTML( |
| <iframe style="width: 275px; height: 250px; will-change: transform"> |
| </iframe> |
| )HTML"); |
| auto* child_frame = |
| To<WebLocalFrameImpl>(GetWebView()->MainFrameImpl()->FirstChild()); |
| frame_test_helpers::LoadHTMLString(child_frame, R"HTML( |
| <p style="margin: 1000px"> Hello </p> |
| <script> |
| window.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| }, {passive: false}); |
| </script> |
| )HTML", |
| url_test_helpers::ToKURL("about:blank")); |
| ForceFullCompositingUpdate(); |
| |
| const auto* child_cc_layer = |
| FrameScrollingContentsLayer(*child_frame->GetFrame()); |
| cc::Region region_child_frame = |
| child_cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| cc::Region region_main_frame = |
| MainFrameScrollingContentsLayer() |
| ->touch_action_region() |
| .GetRegionForTouchAction(TouchAction::kNone); |
| EXPECT_TRUE(region_main_frame.bounds().IsEmpty()); |
| EXPECT_FALSE(region_child_frame.bounds().IsEmpty()); |
| // We only check for the content size for verification as the offset is 0x0 |
| // due to child frame having its own composited layer. |
| |
| // Because touch action rects are painted on the scrolling contents layer, |
| // the size of the rect should be equal to the entire scrolling contents area. |
| EXPECT_EQ(gfx::Rect(child_cc_layer->bounds()), region_child_frame.bounds()); |
| } |
| |
| TEST_P(ScrollingTest, WindowTouchEventHandler) { |
| LoadHTML(R"HTML( |
| <style> |
| html { width: 200px; height: 200px; } |
| body { width: 100px; height: 100px; } |
| </style> |
| <script> |
| window.addEventListener('touchstart', function(event) { |
| event.preventDefault(); |
| }, {passive: false} ); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| // The touch action region should include the entire frame, even though the |
| // document is smaller than the frame. |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 320, 240)); |
| } |
| |
| namespace { |
| class ScrollingTestMockEventListener final : public NativeEventListener { |
| public: |
| void Invoke(ExecutionContext*, Event*) override {} |
| }; |
| } // namespace |
| |
| TEST_P(ScrollingTest, WindowTouchEventHandlerInvalidation) { |
| LoadHTML(""); |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| // Initially there are no touch action regions. |
| auto region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| // Adding a blocking window event handler should create a touch action region. |
| auto* listener = MakeGarbageCollected<ScrollingTestMockEventListener>(); |
| auto* resolved_options = |
| MakeGarbageCollected<AddEventListenerOptionsResolved>(); |
| resolved_options->setPassive(false); |
| GetFrame()->DomWindow()->addEventListener(event_type_names::kTouchstart, |
| listener, resolved_options); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_FALSE(region.IsEmpty()); |
| |
| // Removing the window event handler also removes the blocking touch action |
| // region. |
| GetFrame()->DomWindow()->RemoveAllEventListeners(); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, TouchActionChangeWithoutContent) { |
| LoadHTML(R"HTML( |
| <div id="blocking" |
| style="will-change: transform; width: 100px; height: 100px;"></div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| // Adding a blocking window event handler should create a touch action region. |
| auto* listener = MakeGarbageCollected<ScrollingTestMockEventListener>(); |
| auto* resolved_options = |
| MakeGarbageCollected<AddEventListenerOptionsResolved>(); |
| resolved_options->setPassive(false); |
| auto* target_element = GetFrame()->GetDocument()->getElementById("blocking"); |
| target_element->addEventListener(event_type_names::kTouchstart, listener, |
| resolved_options); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = LayerByDOMElementId("blocking"); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, WheelEventRegion) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <style> |
| #scrollable { |
| width: 200px; |
| height: 200px; |
| will-change: transform; |
| overflow: scroll; |
| } |
| #content { |
| width: 1000px; |
| height: 1000px; |
| } |
| </style> |
| <div id="scrollable"> |
| <div id="content"></div> |
| </div> |
| <script> |
| document.getElementById("scrollable").addEventListener('wheel', (e) => { |
| e.preventDefault(); |
| }); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = MainFrameScrollingContentsLayer(); |
| cc::Region region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| cc_layer = LayerByDOMElementId("scrollable"); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 200, 200)); |
| |
| cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.GetRegionComplexity(), 1); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 1000, 1000)); |
| } |
| |
| TEST_P(ScrollingTest, WheelEventHandlerInvalidation) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <style> |
| #scrollable { |
| width: 200px; |
| height: 200px; |
| will-change: transform; |
| overflow: scroll; |
| } |
| #content { |
| width: 1000px; |
| height: 1000px; |
| } |
| </style> |
| <div id="scrollable"> |
| <div id="content"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| // Initially there are no wheel event regions. |
| const auto* cc_layer = LayerByDOMElementId("scrollable"); |
| auto region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| const auto* cc_layer_content = |
| ScrollingContentsLayerByDOMElementId("scrollable"); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| // Adding a blocking window event handler should create a wheel event region. |
| auto* listener = MakeGarbageCollected<ScrollingTestMockEventListener>(); |
| auto* resolved_options = |
| MakeGarbageCollected<AddEventListenerOptionsResolved>(); |
| resolved_options->setPassive(false); |
| GetFrame() |
| ->GetDocument() |
| ->getElementById("scrollable") |
| ->addEventListener(event_type_names::kWheel, listener, resolved_options); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_FALSE(region.IsEmpty()); |
| region = cc_layer_content->wheel_event_region(); |
| EXPECT_FALSE(region.IsEmpty()); |
| |
| // Removing the window event handler also removes the wheel event region. |
| GetFrame() |
| ->GetDocument() |
| ->getElementById("scrollable") |
| ->RemoveAllEventListeners(); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| region = cc_layer_content->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, WheelEventRegions) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <style> |
| #scrollable { |
| width: 200px; |
| height: 200px; |
| will-change: transform; |
| overflow: scroll; |
| } |
| #content { |
| width: 1000px; |
| height: 1000px; |
| } |
| .region { |
| width: 100px; |
| height: 100px; |
| } |
| </style> |
| <div id="scrollable"> |
| <div id="region1" class="region"></div> |
| <div id="content"></div> |
| <div id="region2" class="region"></div> |
| </div> |
| <script> |
| document.getElementById("region1").addEventListener('wheel', (e) => { |
| e.preventDefault(); |
| }); |
| document.getElementById("region2").addEventListener('wheel', (e) => { |
| e.preventDefault(); |
| }); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = LayerByDOMElementId("scrollable"); |
| cc::Region region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| cc_layer = ScrollingContentsLayerByDOMElementId("scrollable"); |
| region = cc_layer->wheel_event_region(); |
| |
| cc::Region expected_region; |
| expected_region.Union(gfx::Rect(0, 0, 100, 100)); |
| expected_region.Union(gfx::Rect(0, 1100, 100, 100)); |
| |
| EXPECT_EQ(region, expected_region); |
| } |
| |
| TEST_P(ScrollingTest, WheelEventRegionUpdatedOnSubscrollerScrollChange) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| GetWebView()->GetSettings()->SetPreferCompositingToLCDTextEnabled(false); |
| LoadHTML(R"HTML( |
| <style> |
| #noncomposited { |
| width: 200px; |
| height: 200px; |
| overflow: auto; |
| position: absolute; |
| top: 50px; |
| background: white; |
| box-shadow: 10px 10px black inset; |
| } |
| #content { |
| width: 100%; |
| height: 1000px; |
| } |
| .region { |
| width: 100px; |
| height: 100px; |
| } |
| </style> |
| <div id="noncomposited"> |
| <div id="region" class="region"></div> |
| <div id="content"></div> |
| </div> |
| <script> |
| document.getElementById("region").addEventListener('wheel', (e) => { |
| e.preventDefault(); |
| }, {passive: false}); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| cc::Region region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 50, 100, 100)); |
| |
| Element* scrollable_element = |
| GetFrame()->GetDocument()->getElementById("noncomposited"); |
| DCHECK(scrollable_element); |
| |
| // Change scroll position and verify that blocking wheel handler region is |
| // updated accordingly. |
| scrollable_element->setScrollTop(10.0); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 50, 100, 90)); |
| } |
| |
| // Box shadow is not hit testable and should not be included in wheel region. |
| TEST_P(ScrollingTest, WheelEventRegionExcludesBoxShadow) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <style> |
| #shadow { |
| width: 100px; |
| height: 100px; |
| box-shadow: 10px 5px 5px red; |
| } |
| </style> |
| <div id="shadow"></div> |
| <script> |
| document.getElementById("shadow").addEventListener('wheel', (e) => { |
| e.preventDefault(); |
| }); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| cc::Region region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(8, 8, 100, 100)); |
| } |
| |
| TEST_P(ScrollingTest, IframeWindowWheelEventHandler) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <iframe style="width: 275px; height: 250px; will-change: transform"> |
| </iframe> |
| )HTML"); |
| auto* child_frame = |
| To<WebLocalFrameImpl>(GetWebView()->MainFrameImpl()->FirstChild()); |
| frame_test_helpers::LoadHTMLString(child_frame, R"HTML( |
| <p style="margin: 1000px"> Hello </p> |
| <script> |
| window.addEventListener('wheel', (e) => { |
| e.preventDefault(); |
| }, {passive: false}); |
| </script> |
| )HTML", |
| url_test_helpers::ToKURL("about:blank")); |
| ForceFullCompositingUpdate(); |
| |
| const auto* child_cc_layer = |
| FrameScrollingContentsLayer(*child_frame->GetFrame()); |
| cc::Region region_child_frame = child_cc_layer->wheel_event_region(); |
| cc::Region region_main_frame = |
| MainFrameScrollingContentsLayer()->wheel_event_region(); |
| EXPECT_TRUE(region_main_frame.bounds().IsEmpty()); |
| EXPECT_FALSE(region_child_frame.bounds().IsEmpty()); |
| // We only check for the content size for verification as the offset is 0x0 |
| // due to child frame having its own composited layer. |
| |
| // Because blocking wheel rects are painted on the scrolling contents layer, |
| // the size of the rect should be equal to the entire scrolling contents area. |
| EXPECT_EQ(gfx::Rect(child_cc_layer->bounds()), region_child_frame.bounds()); |
| } |
| |
| TEST_P(ScrollingTest, WindowWheelEventHandler) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <style> |
| html { width: 200px; height: 200px; } |
| body { width: 100px; height: 100px; } |
| </style> |
| <script> |
| window.addEventListener('wheel', function(event) { |
| event.preventDefault(); |
| }, {passive: false} ); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| // The wheel region should include the entire frame, even though the |
| // document is smaller than the frame. |
| cc::Region region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 320, 240)); |
| } |
| |
| TEST_P(ScrollingTest, WindowWheelEventHandlerInvalidation) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(""); |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = MainFrameScrollingContentsLayer(); |
| |
| // Initially there are no wheel event regions. |
| auto region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| |
| // Adding a blocking window event handler should create a wheel event region. |
| auto* listener = MakeGarbageCollected<ScrollingTestMockEventListener>(); |
| auto* resolved_options = |
| MakeGarbageCollected<AddEventListenerOptionsResolved>(); |
| resolved_options->setPassive(false); |
| GetFrame()->DomWindow()->addEventListener(event_type_names::kWheel, listener, |
| resolved_options); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_FALSE(region.IsEmpty()); |
| |
| // Removing the window event handler also removes the wheel event region. |
| GetFrame()->DomWindow()->RemoveAllEventListeners(); |
| ForceFullCompositingUpdate(); |
| region = cc_layer->wheel_event_region(); |
| EXPECT_TRUE(region.IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, WheelEventHandlerChangeWithoutContent) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions); |
| LoadHTML(R"HTML( |
| <div id="blocking" |
| style="will-change: transform; width: 100px; height: 100px;"></div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| // Adding a blocking window event handler should create a touch action region. |
| auto* listener = MakeGarbageCollected<ScrollingTestMockEventListener>(); |
| auto* resolved_options = |
| MakeGarbageCollected<AddEventListenerOptionsResolved>(); |
| resolved_options->setPassive(false); |
| auto* target_element = GetFrame()->GetDocument()->getElementById("blocking"); |
| target_element->addEventListener(event_type_names::kWheel, listener, |
| resolved_options); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = LayerByDOMElementId("blocking"); |
| cc::Region region = cc_layer->wheel_event_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 100, 100)); |
| } |
| |
| // Ensure we don't crash when a plugin becomes a LayoutInline |
| TEST_P(ScrollingTest, PluginBecomesLayoutInline) { |
| LoadHTML(R"HTML( |
| <style> |
| body { |
| margin: 0; |
| height: 3000px; |
| } |
| </style> |
| <object id="plugin" type="application/x-webkit-test-plugin"></object> |
| <script> |
| document.getElementById("plugin") |
| .appendChild(document.createElement("label")) |
| </script> |
| )HTML"); |
| |
| // This test passes if it doesn't crash. We're trying to make sure |
| // ScrollingCoordinator can deal with LayoutInline plugins when generating |
| // NonFastScrollableRegions. |
| auto* plugin = To<HTMLObjectElement>( |
| GetFrame()->GetDocument()->getElementById("plugin")); |
| ASSERT_TRUE(plugin->GetLayoutObject()->IsLayoutInline()); |
| ForceFullCompositingUpdate(); |
| } |
| |
| // Ensure NonFastScrollableRegions are correctly generated for both fixed and |
| // in-flow plugins that need them. |
| TEST_P(ScrollingTest, NonFastScrollableRegionsForPlugins) { |
| LoadHTML(R"HTML( |
| <style> |
| body { |
| margin: 0; |
| height: 3000px; |
| } |
| #plugin { |
| width: 300px; |
| height: 300px; |
| } |
| #pluginfixed { |
| width: 200px; |
| height: 200px; |
| } |
| #fixed { |
| position: fixed; |
| top: 500px; |
| } |
| </style> |
| <div id="fixed"> |
| <object id="pluginfixed" type="application/x-webkit-test-plugin"></object> |
| </div> |
| <object id="plugin" type="application/x-webkit-test-plugin"></object> |
| )HTML"); |
| |
| auto* plugin = To<HTMLObjectElement>( |
| GetFrame()->GetDocument()->getElementById("plugin")); |
| auto* plugin_fixed = To<HTMLObjectElement>( |
| GetFrame()->GetDocument()->getElementById("pluginfixed")); |
| // NonFastScrollableRegions are generated for plugins that require wheel |
| // events. |
| plugin->OwnedPlugin()->SetWantsWheelEvents(true); |
| plugin_fixed->OwnedPlugin()->SetWantsWheelEvents(true); |
| |
| ForceFullCompositingUpdate(); |
| |
| // The non-fixed plugin should create a non-fast scrollable region in the |
| // scrolling contents layer of the LayoutView. |
| auto* viewport_non_fast_layer = MainFrameScrollingContentsLayer(); |
| EXPECT_EQ(viewport_non_fast_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(0, 0, 300, 300)); |
| |
| // The fixed plugin should create a non-fast scrollable region in a fixed |
| // cc::Layer. |
| auto* fixed_layer = LayerByDOMElementId("fixed"); |
| EXPECT_EQ(fixed_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(0, 0, 200, 200)); |
| } |
| |
| TEST_P(ScrollingTest, NonFastScrollableRegionWithBorder) { |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| body { margin: 0; } |
| #scroller { |
| height: 100px; |
| width: 100px; |
| overflow-y: scroll; |
| border: 10px solid black; |
| } |
| </style> |
| <div id="scroller"> |
| <div id="forcescroll" style="height: 1000px;"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* non_fast_layer = MainFrameScrollingContentsLayer(); |
| EXPECT_EQ(non_fast_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(0, 0, 120, 120)); |
| } |
| |
| TEST_P(ScrollingTest, overflowScrolling) { |
| RegisterMockedHttpURLLoad("overflow-scrolling.html"); |
| NavigateTo(base_url_ + "overflow-scrolling.html"); |
| ForceFullCompositingUpdate(); |
| |
| // Verify the scroll node of the accelerated scrolling element. |
| const auto* scroll_node = ScrollNodeByDOMElementId("scrollable"); |
| ASSERT_TRUE(scroll_node); |
| EXPECT_TRUE(scroll_node->user_scrollable_horizontal); |
| EXPECT_TRUE(scroll_node->user_scrollable_vertical); |
| |
| EXPECT_TRUE(ScrollbarLayerForScrollNode( |
| scroll_node, cc::ScrollbarOrientation::HORIZONTAL)); |
| EXPECT_TRUE(ScrollbarLayerForScrollNode(scroll_node, |
| cc::ScrollbarOrientation::VERTICAL)); |
| } |
| |
| TEST_P(ScrollingTest, overflowHidden) { |
| RegisterMockedHttpURLLoad("overflow-hidden.html"); |
| NavigateTo(base_url_ + "overflow-hidden.html"); |
| ForceFullCompositingUpdate(); |
| |
| // Verify the scroll node of the accelerated scrolling element. |
| const auto* scroll_node = ScrollNodeByDOMElementId("unscrollable-y"); |
| ASSERT_TRUE(scroll_node); |
| EXPECT_TRUE(scroll_node->user_scrollable_horizontal); |
| EXPECT_FALSE(scroll_node->user_scrollable_vertical); |
| |
| scroll_node = ScrollNodeByDOMElementId("unscrollable-x"); |
| ASSERT_TRUE(scroll_node); |
| EXPECT_FALSE(scroll_node->user_scrollable_horizontal); |
| EXPECT_TRUE(scroll_node->user_scrollable_vertical); |
| } |
| |
| TEST_P(ScrollingTest, iframeScrolling) { |
| RegisterMockedHttpURLLoad("iframe-scrolling.html"); |
| RegisterMockedHttpURLLoad("iframe-scrolling-inner.html"); |
| NavigateTo(base_url_ + "iframe-scrolling.html"); |
| ForceFullCompositingUpdate(); |
| |
| Element* scrollable_frame = |
| GetFrame()->GetDocument()->getElementById("scrollable"); |
| ASSERT_TRUE(scrollable_frame); |
| |
| LayoutObject* layout_object = scrollable_frame->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| ASSERT_TRUE(layout_object->IsLayoutEmbeddedContent()); |
| |
| auto* layout_embedded_content = To<LayoutEmbeddedContent>(layout_object); |
| ASSERT_TRUE(layout_embedded_content); |
| |
| LocalFrameView* inner_frame_view = |
| To<LocalFrameView>(layout_embedded_content->ChildFrameView()); |
| ASSERT_TRUE(inner_frame_view); |
| |
| // Verify the scroll node of the accelerated scrolling iframe. |
| const auto* scroll_node = |
| ScrollNodeForScrollableArea(inner_frame_view->LayoutViewport()); |
| ASSERT_TRUE(scroll_node); |
| EXPECT_TRUE(ScrollbarLayerForScrollNode( |
| scroll_node, cc::ScrollbarOrientation::HORIZONTAL)); |
| EXPECT_TRUE(ScrollbarLayerForScrollNode(scroll_node, |
| cc::ScrollbarOrientation::VERTICAL)); |
| } |
| |
| TEST_P(ScrollingTest, rtlIframe) { |
| RegisterMockedHttpURLLoad("rtl-iframe.html"); |
| RegisterMockedHttpURLLoad("rtl-iframe-inner.html"); |
| NavigateTo(base_url_ + "rtl-iframe.html"); |
| ForceFullCompositingUpdate(); |
| |
| Element* scrollable_frame = |
| GetFrame()->GetDocument()->getElementById("scrollable"); |
| ASSERT_TRUE(scrollable_frame); |
| |
| LayoutObject* layout_object = scrollable_frame->GetLayoutObject(); |
| ASSERT_TRUE(layout_object); |
| ASSERT_TRUE(layout_object->IsLayoutEmbeddedContent()); |
| |
| auto* layout_embedded_content = To<LayoutEmbeddedContent>(layout_object); |
| ASSERT_TRUE(layout_embedded_content); |
| |
| LocalFrameView* inner_frame_view = |
| To<LocalFrameView>(layout_embedded_content->ChildFrameView()); |
| ASSERT_TRUE(inner_frame_view); |
| |
| // Verify the scroll node of the accelerated scrolling iframe. |
| const auto* scroll_node = |
| ScrollNodeForScrollableArea(inner_frame_view->LayoutViewport()); |
| ASSERT_TRUE(scroll_node); |
| |
| int expected_scroll_position = 958 + (inner_frame_view->LayoutViewport() |
| ->VerticalScrollbar() |
| ->IsOverlayScrollbar() |
| ? 0 |
| : 15); |
| ASSERT_EQ(expected_scroll_position, CurrentScrollOffset(scroll_node).x()); |
| } |
| |
| TEST_P(ScrollingTest, setupScrollbarLayerShouldNotCrash) { |
| RegisterMockedHttpURLLoad("setup_scrollbar_layer_crash.html"); |
| NavigateTo(base_url_ + "setup_scrollbar_layer_crash.html"); |
| ForceFullCompositingUpdate(); |
| // This test document setup an iframe with scrollbars, then switch to |
| // an empty document by javascript. |
| } |
| |
| #if defined(OS_MAC) || defined(OS_ANDROID) |
| TEST_P(ScrollingTest, DISABLED_setupScrollbarLayerShouldSetScrollLayerOpaque) |
| #else |
| TEST_P(ScrollingTest, setupScrollbarLayerShouldSetScrollLayerOpaque) |
| #endif |
| { |
| ScopedMockOverlayScrollbars mock_overlay_scrollbar(false); |
| |
| RegisterMockedHttpURLLoad("wide_document.html"); |
| NavigateTo(base_url_ + "wide_document.html"); |
| ForceFullCompositingUpdate(); |
| |
| LocalFrameView* frame_view = GetFrame()->View(); |
| ASSERT_TRUE(frame_view); |
| |
| const auto* scroll_node = |
| ScrollNodeForScrollableArea(frame_view->LayoutViewport()); |
| ASSERT_TRUE(scroll_node); |
| |
| const auto* horizontal_scrollbar_layer = ScrollbarLayerForScrollNode( |
| scroll_node, cc::ScrollbarOrientation::HORIZONTAL); |
| ASSERT_TRUE(horizontal_scrollbar_layer); |
| // TODO(crbug.com/1029620): CAP needs more accurate contents_opaque. |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) { |
| EXPECT_EQ(!frame_view->LayoutViewport() |
| ->HorizontalScrollbar() |
| ->IsOverlayScrollbar(), |
| horizontal_scrollbar_layer->contents_opaque()); |
| } |
| |
| EXPECT_FALSE(ScrollbarLayerForScrollNode(scroll_node, |
| cc::ScrollbarOrientation::VERTICAL)); |
| } |
| |
| TEST_P(ScrollingTest, NestedIFramesMainThreadScrollingRegion) { |
| // This page has an absolute IFRAME. It contains a scrollable child DIV |
| // that's nested within an intermediate IFRAME. |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #spacer { |
| height: 10000px; |
| } |
| iframe { |
| position: absolute; |
| top: 1200px; |
| left: 0px; |
| width: 200px; |
| height: 200px; |
| border: 0; |
| } |
| |
| </style> |
| <div id="spacer"></div> |
| <iframe srcdoc=" |
| <!DOCTYPE html> |
| <style> |
| body { margin: 0; } |
| iframe { width: 100px; height: 100px; border: 0; } |
| </style> |
| <iframe srcdoc='<!DOCTYPE html> |
| <style> |
| body { margin: 0; } |
| div { |
| width: 65px; |
| height: 65px; |
| overflow: auto; |
| } |
| p { |
| width: 300px; |
| height: 300px; |
| } |
| </style> |
| <div> |
| <p></p> |
| </div>'> |
| </iframe>"> |
| </iframe> |
| )HTML"); |
| |
| ForceFullCompositingUpdate(); |
| |
| // Scroll the frame to ensure the rect is in the correct coordinate space. |
| GetFrame()->GetDocument()->View()->GetScrollableArea()->SetScrollOffset( |
| ScrollOffset(0, 1000), mojom::blink::ScrollType::kProgrammatic); |
| |
| ForceFullCompositingUpdate(); |
| |
| auto* non_fast_layer = MainFrameScrollingContentsLayer(); |
| EXPECT_EQ(non_fast_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(0, 1200, 65, 65)); |
| } |
| |
| // Same as above but test that the rect is correctly calculated into the fixed |
| // region when the containing iframe is position: fixed. |
| TEST_P(ScrollingTest, NestedFixedIFramesMainThreadScrollingRegion) { |
| // This page has a fixed IFRAME. It contains a scrollable child DIV that's |
| // nested within an intermediate IFRAME. |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #spacer { |
| height: 10000px; |
| } |
| #iframe { |
| position: fixed; |
| top: 20px; |
| left: 0px; |
| width: 200px; |
| height: 200px; |
| border: 20px solid blue; |
| } |
| |
| </style> |
| <div id="spacer"></div> |
| <iframe id="iframe" srcdoc=" |
| <!DOCTYPE html> |
| <style> |
| body { margin: 0; } |
| iframe { width: 100px; height: 100px; border: 0; } |
| </style> |
| <iframe srcdoc='<!DOCTYPE html> |
| <style> |
| body { margin: 0; } |
| div { |
| width: 75px; |
| height: 75px; |
| overflow: auto; |
| } |
| p { |
| width: 300px; |
| height: 300px; |
| } |
| </style> |
| <div> |
| <p></p> |
| </div>'> |
| </iframe>"> |
| </iframe> |
| )HTML"); |
| |
| ForceFullCompositingUpdate(); |
| |
| // Scroll the frame to ensure the rect is in the correct coordinate space. |
| GetFrame()->GetDocument()->View()->GetScrollableArea()->SetScrollOffset( |
| ScrollOffset(0, 1000), mojom::blink::ScrollType::kProgrammatic); |
| |
| ForceFullCompositingUpdate(); |
| auto* non_fast_layer = LayerByDOMElementId("iframe"); |
| EXPECT_EQ(non_fast_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(20, 20, 75, 75)); |
| } |
| |
| TEST_P(ScrollingTest, IframeCompositedScrollingHideAndShow) { |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| body { |
| margin: 0; |
| } |
| iframe { |
| height: 100px; |
| width: 100px; |
| } |
| </style> |
| <iframe id="iframe" srcdoc=" |
| <!DOCTYPE html> |
| <style> |
| body {height: 1000px;} |
| </style>"></iframe> |
| )HTML"); |
| |
| ForceFullCompositingUpdate(); |
| |
| const auto* non_fast_layer = MainFrameScrollingContentsLayer(); |
| |
| // Should have a NFSR initially. |
| EXPECT_EQ(non_fast_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(2, 2, 100, 100)); |
| |
| // Hiding the iframe should clear the NFSR. |
| Element* iframe = GetFrame()->GetDocument()->getElementById("iframe"); |
| iframe->setAttribute(html_names::kStyleAttr, "display: none"); |
| ForceFullCompositingUpdate(); |
| EXPECT_TRUE(non_fast_layer->non_fast_scrollable_region().bounds().IsEmpty()); |
| |
| // Showing it again should compute the NFSR. |
| iframe->setAttribute(html_names::kStyleAttr, ""); |
| ForceFullCompositingUpdate(); |
| EXPECT_EQ(non_fast_layer->non_fast_scrollable_region().bounds(), |
| gfx::Rect(2, 2, 100, 100)); |
| } |
| |
| // Same as above but the main frame is scrollable. This should cause the non |
| // fast scrollable regions to go on the outer viewport's scroll layer. |
| TEST_P(ScrollingTest, IframeCompositedScrollingHideAndShowScrollable) { |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| body { |
| height: 1000px; |
| margin: 0; |
| } |
| iframe { |
| height: 100px; |
| width: 100px; |
| } |
| </style> |
| <iframe id="iframe" srcdoc=" |
| <!DOCTYPE html> |
| <style> |
| body {height: 1000px;} |
| </style>"></iframe> |
| )HTML"); |
| |
| ForceFullCompositingUpdate(); |
| |
| Page* page = GetFrame()->GetPage(); |
| const auto* inner_viewport_scroll_layer = |
| page->GetVisualViewport().LayerForScrolling(); |
| Element* iframe = GetFrame()->GetDocument()->getElementById("iframe"); |
| |
| const auto* outer_viewport_scroll_layer = MainFrameScrollingContentsLayer(); |
| |
| // Should have a NFSR initially. |
| ForceFullCompositingUpdate(); |
| EXPECT_FALSE(outer_viewport_scroll_layer->non_fast_scrollable_region() |
| .bounds() |
| .IsEmpty()); |
| |
| // Ensure the visual viewport's scrolling layer didn't get an NFSR. |
| EXPECT_TRUE(inner_viewport_scroll_layer->non_fast_scrollable_region() |
| .bounds() |
| .IsEmpty()); |
| |
| // Hiding the iframe should clear the NFSR. |
| iframe->setAttribute(html_names::kStyleAttr, "display: none"); |
| ForceFullCompositingUpdate(); |
| EXPECT_TRUE(outer_viewport_scroll_layer->non_fast_scrollable_region() |
| .bounds() |
| .IsEmpty()); |
| |
| // Showing it again should compute the NFSR. |
| iframe->setAttribute(html_names::kStyleAttr, ""); |
| ForceFullCompositingUpdate(); |
| EXPECT_FALSE(outer_viewport_scroll_layer->non_fast_scrollable_region() |
| .bounds() |
| .IsEmpty()); |
| } |
| |
| TEST_P(ScrollingTest, ScrollOffsetClobberedBeforeCompositingUpdate) { |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #container { |
| width: 300px; |
| height: 300px; |
| overflow: auto; |
| will-change: transform; |
| } |
| #spacer { |
| height: 1000px; |
| } |
| </style> |
| <div id="container"> |
| <div id="spacer"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* scrollable_area = ScrollableAreaByDOMElementId("container"); |
| ASSERT_EQ(0, scrollable_area->GetScrollOffset().Height()); |
| const auto* scroll_node = ScrollNodeForScrollableArea(scrollable_area); |
| |
| // Simulate 100px of scroll coming from the compositor thread during a commit. |
| gfx::ScrollOffset compositor_delta(0, 100.f); |
| cc::CompositorCommitData commit_data; |
| commit_data.scrolls.push_back( |
| {scrollable_area->GetScrollElementId(), compositor_delta, base::nullopt}); |
| RootCcLayer()->layer_tree_host()->ApplyCompositorChanges(&commit_data); |
| // The compositor offset is reflected in blink and cc scroll tree. |
| EXPECT_EQ(compositor_delta, |
| gfx::ScrollOffset(scrollable_area->ScrollPosition())); |
| EXPECT_EQ(compositor_delta, CurrentScrollOffset(scroll_node)); |
| |
| // Before updating the lifecycle, set the scroll offset back to what it was |
| // before the commit from the main thread. |
| scrollable_area->SetScrollOffset(ScrollOffset(0, 0), |
| mojom::blink::ScrollType::kProgrammatic); |
| |
| // Ensure the offset is up-to-date on the cc::Layer even though, as far as |
| // the main thread is concerned, it was unchanged since the last time we |
| // pushed the scroll offset. |
| ForceFullCompositingUpdate(); |
| EXPECT_EQ(gfx::ScrollOffset(), CurrentScrollOffset(scroll_node)); |
| } |
| |
| TEST_P(ScrollingTest, UpdateVisualViewportScrollLayer) { |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #box { |
| width: 300px; |
| height: 1000px; |
| background-color: red; |
| } |
| </style> |
| <div id="box"> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| Page* page = GetFrame()->GetPage(); |
| const auto* inner_viewport_scroll_node = |
| ScrollNodeForScrollableArea(&page->GetVisualViewport()); |
| |
| page->GetVisualViewport().SetScale(2); |
| ForceFullCompositingUpdate(); |
| EXPECT_EQ(gfx::ScrollOffset(0, 0), |
| CurrentScrollOffset(inner_viewport_scroll_node)); |
| |
| page->GetVisualViewport().SetLocation(FloatPoint(10, 20)); |
| ForceFullCompositingUpdate(); |
| EXPECT_EQ(gfx::ScrollOffset(10, 20), |
| CurrentScrollOffset(inner_viewport_scroll_node)); |
| } |
| |
| TEST_P(ScrollingTest, NonCompositedNonFastScrollableRegion) { |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| body { margin: 0; } |
| #composited_container { |
| will-change: transform; |
| border: 20px solid blue; |
| } |
| #scroller { |
| height: 200px; |
| width: 200px; |
| overflow-y: scroll; |
| } |
| </style> |
| <div id="composited_container"> |
| <div id="scroller"> |
| <div id="forcescroll" style="height: 1000px;"></div> |
| </div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| // The non-scrolling graphics layer should have a non-scrolling region for the |
| // non-composited scroller. |
| const auto* cc_layer = LayerByDOMElementId("composited_container"); |
| auto region = cc_layer->non_fast_scrollable_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(20, 20, 200, 200)); |
| } |
| |
| TEST_P(ScrollingTest, NonCompositedResizerNonFastScrollableRegion) { |
| GetWebView()->GetPage()->GetSettings().SetPreferCompositingToLCDTextEnabled( |
| false); |
| LoadHTML(R"HTML( |
| <style> |
| #container { |
| will-change: transform; |
| border: 20px solid blue; |
| } |
| #scroller { |
| width: 80px; |
| height: 80px; |
| resize: both; |
| overflow-y: scroll; |
| } |
| </style> |
| <div id="container"> |
| <div id="offset" style="height: 35px;"></div> |
| <div id="scroller"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* container_cc_layer = LayerByDOMElementId("container"); |
| // The non-fast scrollable region should be on the container's graphics layer |
| // and not one of the viewport scroll layers because the region should move |
| // when the container moves and not when the viewport scrolls. |
| auto region = container_cc_layer->non_fast_scrollable_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(86, 121, 14, 14)); |
| } |
| |
| TEST_P(ScrollingTest, CompositedResizerNonFastScrollableRegion) { |
| LoadHTML(R"HTML( |
| <style> |
| #container { will-change: transform; } |
| #scroller { |
| will-change: transform; |
| width: 80px; |
| height: 80px; |
| resize: both; |
| overflow-y: scroll; |
| } |
| </style> |
| <div id="container"> |
| <div id="offset" style="height: 35px;"></div> |
| <div id="scroller"></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto region = LayerByDOMElementId("scroller")->non_fast_scrollable_region(); |
| EXPECT_EQ(region.bounds(), gfx::Rect(66, 66, 14, 14)); |
| } |
| |
| TEST_P(ScrollingTest, TouchActionUpdatesOutsideInterestRect) { |
| LoadHTML(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #scroller { |
| will-change: transform; |
| width: 200px; |
| height: 200px; |
| background: blue; |
| overflow-y: scroll; |
| } |
| .spacer { |
| height: 1000px; |
| } |
| #touchaction { |
| height: 100px; |
| background: yellow; |
| } |
| </style> |
| <div id="scroller"> |
| <div class="spacer"></div> |
| <div class="spacer"></div> |
| <div class="spacer"></div> |
| <div class="spacer"></div> |
| <div class="spacer"></div> |
| <div id="touchaction">This should not scroll via touch.</div> |
| </div> |
| )HTML"); |
| |
| ForceFullCompositingUpdate(); |
| |
| auto* touch_action = GetFrame()->GetDocument()->getElementById("touchaction"); |
| touch_action->setAttribute(html_names::kStyleAttr, "touch-action: none;"); |
| |
| ForceFullCompositingUpdate(); |
| |
| ScrollableAreaByDOMElementId("scroller") |
| ->SetScrollOffset(ScrollOffset(0, 5100), |
| mojom::blink::ScrollType::kProgrammatic); |
| |
| ForceFullCompositingUpdate(); |
| |
| auto* cc_layer = ScrollingContentsLayerByDOMElementId("scroller"); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 5000, 200, 100)); |
| } |
| |
| TEST_P(ScrollingTest, MainThreadScrollAndDeltaFromImplSide) { |
| LoadHTML(R"HTML( |
| <div id='scroller' style='overflow: scroll; width: 100px; height: 100px'> |
| <div style='height: 1000px'></div> |
| </div> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| auto* scroller = GetFrame()->GetDocument()->getElementById("scroller"); |
| auto* scrollable_area = scroller->GetLayoutBox()->GetScrollableArea(); |
| auto element_id = scrollable_area->GetScrollElementId(); |
| |
| EXPECT_EQ(gfx::ScrollOffset(), CurrentScrollOffset(element_id)); |
| |
| // Simulate a direct scroll update out of document lifecycle update. |
| scroller->scrollTo(0, 200); |
| EXPECT_EQ(FloatPoint(0, 200), scrollable_area->ScrollPosition()); |
| EXPECT_EQ(gfx::ScrollOffset(0, 200), CurrentScrollOffset(element_id)); |
| |
| // Simulate the scroll update with scroll delta from impl-side at the |
| // beginning of BeginMainFrame. |
| cc::CompositorCommitData commit_data; |
| commit_data.scrolls.push_back(cc::CompositorCommitData::ScrollUpdateInfo( |
| element_id, gfx::ScrollOffset(0, 10), base::nullopt)); |
| RootCcLayer()->layer_tree_host()->ApplyCompositorChanges(&commit_data); |
| EXPECT_EQ(FloatPoint(0, 210), scrollable_area->ScrollPosition()); |
| EXPECT_EQ(gfx::ScrollOffset(0, 210), CurrentScrollOffset(element_id)); |
| } |
| |
| class UnifiedScrollingSimTest : public SimTest, public PaintTestConfigurations { |
| public: |
| UnifiedScrollingSimTest() : scroll_unification_enabled_(true) {} |
| |
| void SetUp() override { |
| SimTest::SetUp(); |
| WebView().GetSettings()->SetPreferCompositingToLCDTextEnabled(false); |
| WebView().MainFrameViewWidget()->Resize(gfx::Size(1000, 1000)); |
| WebView().MainFrameViewWidget()->UpdateAllLifecyclePhases( |
| DocumentUpdateReason::kTest); |
| } |
| |
| void RunIdleTasks() { |
| auto* scheduler = |
| ThreadScheduler::Current()->GetWebMainThreadSchedulerForTest(); |
| blink::scheduler::RunIdleTasksForTesting(scheduler, |
| base::BindOnce([]() {})); |
| test::RunPendingTasks(); |
| } |
| |
| const cc::Layer* RootCcLayer() { return GetDocument().View()->RootCcLayer(); } |
| |
| const cc::ScrollNode* ScrollNodeForScrollableArea( |
| const ScrollableArea* scrollable_area) { |
| if (!scrollable_area) |
| return nullptr; |
| const auto* property_trees = |
| RootCcLayer()->layer_tree_host()->property_trees(); |
| return property_trees->scroll_tree.Node( |
| property_trees->element_id_to_scroll_node_index.at( |
| scrollable_area->GetScrollElementId())); |
| } |
| |
| PaintLayerScrollableArea* ScrollableAreaByDOMElementId(const char* id_value) { |
| auto* box = MainFrame() |
| .GetFrame() |
| ->GetDocument() |
| ->getElementById(id_value) |
| ->GetLayoutBoxForScrolling(); |
| return box ? box->GetScrollableArea() : nullptr; |
| } |
| |
| protected: |
| RuntimeEnabledFeaturesTestHelpers::ScopedScrollUnification |
| scroll_unification_enabled_; |
| }; |
| |
| INSTANTIATE_PAINT_TEST_SUITE_P(UnifiedScrollingSimTest); |
| |
| // Tests that the compositor gets a scroll node for noncomposited scrollers by |
| // loading a page with a scroller that has an inset box-shadow, and ensuring |
| // that scroller generates a compositor scroll node with the proper |
| // noncomposited reasons set. It then removes the box-shadow property and |
| // ensures the compositor node updates accordingly. |
| TEST_P(UnifiedScrollingSimTest, ScrollNodeForNonCompositedScroller) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #noncomposited { |
| width: 200px; |
| height: 200px; |
| overflow: auto; |
| position: absolute; |
| top: 300px; |
| background: white; |
| box-shadow: 10px 10px black inset; |
| } |
| #spacer { |
| width: 100%; |
| height: 10000px; |
| } |
| </style> |
| <div id="noncomposited"> |
| <div id="spacer"></div> |
| </div> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| Element* noncomposited_element = |
| MainFrame().GetFrame()->GetDocument()->getElementById("noncomposited"); |
| auto* scrollable_area = |
| noncomposited_element->GetLayoutBoxForScrolling()->GetScrollableArea(); |
| ASSERT_EQ( |
| cc::MainThreadScrollingReason::kCantPaintScrollingBackgroundAndLCDText, |
| scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| |
| const auto* scroll_node = ScrollNodeForScrollableArea(scrollable_area); |
| ASSERT_TRUE(scroll_node); |
| |
| EXPECT_EQ(scroll_node->element_id, scrollable_area->GetScrollElementId()); |
| EXPECT_FALSE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*scroll_node)); |
| |
| // Now remove the box-shadow property and ensure the compositor scroll node |
| // changes. |
| noncomposited_element->setAttribute(html_names::kStyleAttr, |
| "box-shadow: none"); |
| Compositor().BeginFrame(); |
| |
| EXPECT_EQ(0u, scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| EXPECT_EQ(scroll_node->element_id, scrollable_area->GetScrollElementId()); |
| EXPECT_TRUE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*scroll_node)); |
| } |
| |
| // Tests that the compositor retains the scroll node for a composited scroller |
| // when it becomes noncomposited, and ensures the scroll node has its |
| // IsComposited state updated accordingly. |
| TEST_P(UnifiedScrollingSimTest, |
| ScrollNodeForCompositedToNonCompositedScroller) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #composited { |
| width: 200px; |
| height: 200px; |
| overflow: auto; |
| position: absolute; |
| top: 300px; |
| background: white; |
| } |
| #spacer { |
| width: 100%; |
| height: 10000px; |
| } |
| </style> |
| <div id="composited"> |
| <div id="spacer"></div> |
| </div> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| Element* composited_element = |
| MainFrame().GetFrame()->GetDocument()->getElementById("composited"); |
| auto* scrollable_area = |
| composited_element->GetLayoutBoxForScrolling()->GetScrollableArea(); |
| EXPECT_EQ(0u, scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| |
| const auto* scroll_node = ScrollNodeForScrollableArea(scrollable_area); |
| ASSERT_TRUE(scroll_node); |
| |
| EXPECT_EQ(scroll_node->element_id, scrollable_area->GetScrollElementId()); |
| EXPECT_TRUE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*scroll_node)); |
| |
| // Now add an inset box-shadow property to make the node noncomposited and |
| // ensure the compositor scroll node updates accordingly. |
| composited_element->setAttribute(html_names::kStyleAttr, |
| "box-shadow: 10px 10px black inset"); |
| Compositor().BeginFrame(); |
| |
| ASSERT_EQ( |
| cc::MainThreadScrollingReason::kCantPaintScrollingBackgroundAndLCDText, |
| scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| EXPECT_EQ(scroll_node->element_id, scrollable_area->GetScrollElementId()); |
| EXPECT_FALSE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*scroll_node)); |
| } |
| |
| // Tests that the compositor gets a scroll node for noncomposited scrollers |
| // embedded in an iframe, by loading a document with an iframe that has a |
| // scroller with an inset box shadow, and ensuring that scroller generates a |
| // compositor scroll node with the proper noncomposited reasons set. |
| TEST_P(UnifiedScrollingSimTest, ScrollNodeForEmbeddedScrollers) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #iframe { |
| width: 300px; |
| height: 300px; |
| overflow: auto; |
| } |
| </style> |
| <iframe id="iframe" srcdoc=" |
| <!DOCTYPE html> |
| <style> |
| body { |
| background: white; |
| } |
| #scroller { |
| width: 200px; |
| height: 200px; |
| overflow: auto; |
| position: absolute; |
| top: 50px; |
| background: white; |
| box-shadow: 10px 10px black inset; |
| } |
| #spacer { |
| width: 100%; |
| height: 10000px; |
| } |
| </style> |
| <div id='scroller'> |
| <div id='spacer'></div> |
| </div> |
| <div id='spacer'></div>"> |
| </iframe> |
| )HTML"); |
| |
| // RunIdleTasks to load the srcdoc iframe. |
| RunIdleTasks(); |
| Compositor().BeginFrame(); |
| |
| HTMLFrameOwnerElement* iframe = |
| To<HTMLFrameOwnerElement>(GetDocument().getElementById("iframe")); |
| auto* iframe_scrollable_area = |
| iframe->contentDocument()->View()->LayoutViewport(); |
| const auto* iframe_scroll_node = |
| ScrollNodeForScrollableArea(iframe_scrollable_area); |
| ASSERT_TRUE(iframe_scroll_node); |
| |
| // The iframe itself is a composited scroller. |
| EXPECT_EQ( |
| 0u, iframe_scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| EXPECT_EQ(iframe_scroll_node->element_id, |
| iframe_scrollable_area->GetScrollElementId()); |
| EXPECT_TRUE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*iframe_scroll_node)); |
| |
| // Ensure we have a compositor scroll node for the noncomposited subscroller. |
| auto* child_scrollable_area = iframe->contentDocument() |
| ->getElementById("scroller") |
| ->GetLayoutBoxForScrolling() |
| ->GetScrollableArea(); |
| const auto* child_scroll_node = |
| ScrollNodeForScrollableArea(child_scrollable_area); |
| ASSERT_TRUE(child_scroll_node); |
| |
| EXPECT_EQ( |
| cc::MainThreadScrollingReason::kCantPaintScrollingBackgroundAndLCDText, |
| child_scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| EXPECT_EQ(child_scroll_node->element_id, |
| child_scrollable_area->GetScrollElementId()); |
| |
| EXPECT_FALSE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*child_scroll_node)); |
| } |
| |
| // Similar to the above test, but for deeper nesting iframes to ensure we |
| // generate scroll nodes that are deeper than the main frame's children. |
| TEST_P(UnifiedScrollingSimTest, ScrollNodeForNestedEmbeddedScrollers) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest child_request_1("https://example.com/child1.html", "text/html"); |
| SimRequest child_request_2("https://example.com/child2.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| iframe { |
| width: 300px; |
| height: 300px; |
| overflow: auto; |
| } |
| </style> |
| <iframe id="child1" src="child1.html"> |
| )HTML"); |
| |
| child_request_1.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| iframe { |
| width: 300px; |
| height: 300px; |
| overflow: auto; |
| } |
| </style> |
| <iframe id="child2" src="child2.html"> |
| )HTML"); |
| |
| child_request_2.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #scroller { |
| width: 200px; |
| height: 200px; |
| overflow: auto; |
| position: absolute; |
| top: 50px; |
| background: white; |
| box-shadow: 10px 10px black inset; |
| } |
| #spacer { |
| width: 100%; |
| height: 10000px; |
| } |
| </style> |
| <div id='scroller'> |
| <div id='spacer'></div> |
| </div> |
| <div id='spacer'></div> |
| )HTML"); |
| |
| RunIdleTasks(); |
| Compositor().BeginFrame(); |
| |
| HTMLFrameOwnerElement* child_iframe_1 = |
| To<HTMLFrameOwnerElement>(GetDocument().getElementById("child1")); |
| |
| HTMLFrameOwnerElement* child_iframe_2 = To<HTMLFrameOwnerElement>( |
| child_iframe_1->contentDocument()->getElementById("child2")); |
| |
| // Ensure we have a compositor scroll node for the noncomposited subscroller |
| // nested in the second iframe. |
| auto* child_scrollable_area = child_iframe_2->contentDocument() |
| ->getElementById("scroller") |
| ->GetLayoutBoxForScrolling() |
| ->GetScrollableArea(); |
| const auto* child_scroll_node = |
| ScrollNodeForScrollableArea(child_scrollable_area); |
| ASSERT_TRUE(child_scroll_node); |
| |
| EXPECT_EQ( |
| cc::MainThreadScrollingReason::kCantPaintScrollingBackgroundAndLCDText, |
| child_scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| EXPECT_EQ(child_scroll_node->element_id, |
| child_scrollable_area->GetScrollElementId()); |
| |
| EXPECT_FALSE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*child_scroll_node)); |
| } |
| |
| // Tests that the compositor gets a scroll node for opacity 0 noncomposited |
| // scrollers by loading a page with an opacity 0 scroller that has an inset |
| // box-shadow, and ensuring that scroller generates a compositor scroll node |
| // with the proper noncomposited reasons set. The test also ensures that there |
| // is no scroll node for a display:none scroller, as there is no scrollable |
| // area. |
| TEST_P(UnifiedScrollingSimTest, ScrollNodeForInvisibleNonCompositedScroller) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| .noncomposited { |
| width: 200px; |
| height: 200px; |
| overflow: auto; |
| position: absolute; |
| top: 300px; |
| background: white; |
| box-shadow: 10px 10px black inset; |
| } |
| #invisible { |
| opacity: 0; |
| } |
| #displaynone { |
| display: none; |
| } |
| #spacer { |
| width: 100%; |
| height: 10000px; |
| } |
| </style> |
| <div id="invisible" class="noncomposited"> |
| <div id="spacer"></div> |
| </div> |
| <div id="displaynone" class="noncomposited"> |
| <div id="spacer"></div> |
| </div> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| // Ensure the opacity 0 noncomposited scrollable area generates a scroll node |
| auto* invisible_scrollable_area = ScrollableAreaByDOMElementId("invisible"); |
| ASSERT_EQ( |
| cc::MainThreadScrollingReason::kCantPaintScrollingBackgroundAndLCDText, |
| invisible_scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| |
| const auto* invisible_scroll_node = |
| ScrollNodeForScrollableArea(invisible_scrollable_area); |
| ASSERT_TRUE(invisible_scroll_node); |
| |
| EXPECT_EQ(invisible_scroll_node->element_id, |
| invisible_scrollable_area->GetScrollElementId()); |
| |
| EXPECT_FALSE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*invisible_scroll_node)); |
| |
| // Ensure there's no scrollable area (and therefore no scroll node) for a |
| // display none scroller. |
| EXPECT_EQ(nullptr, ScrollableAreaByDOMElementId("displaynone")); |
| } |
| |
| // Tests that the compositor gets a scroll node for scrollable input boxes, |
| // which are unique as they are not a composited scroller but also do not have |
| // NonCompositedMainThreadScrollingReasons. |
| TEST_P(UnifiedScrollingSimTest, ScrollNodeForInputBox) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| input { |
| width: 50px; |
| } |
| </style> |
| <input id="textinput" type="text" value="some overflowing text"/> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* scrollable_area = ScrollableAreaByDOMElementId("textinput"); |
| ASSERT_EQ(0u, scrollable_area->GetNonCompositedMainThreadScrollingReasons()); |
| |
| const auto* scroll_node = ScrollNodeForScrollableArea(scrollable_area); |
| ASSERT_TRUE(scroll_node); |
| |
| EXPECT_EQ(scroll_node->element_id, scrollable_area->GetScrollElementId()); |
| EXPECT_FALSE(RootCcLayer() |
| ->layer_tree_host() |
| ->property_trees() |
| ->scroll_tree.IsComposited(*scroll_node)); |
| } |
| |
| class ScrollingSimTest : public SimTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| ScrollingSimTest() : scroll_unification_enabled_(GetParam()) {} |
| |
| void SetUp() override { |
| SimTest::SetUp(); |
| WebView().GetSettings()->SetPreferCompositingToLCDTextEnabled(true); |
| WebView().MainFrameViewWidget()->Resize(gfx::Size(1000, 1000)); |
| WebView().MainFrameViewWidget()->UpdateAllLifecyclePhases( |
| DocumentUpdateReason::kTest); |
| } |
| |
| WebCoalescedInputEvent GenerateGestureEvent(WebInputEvent::Type type, |
| int delta_x = 0, |
| int delta_y = 0) { |
| WebGestureEvent event(type, WebInputEvent::kNoModifiers, |
| WebInputEvent::GetStaticTimeStampForTests(), |
| WebGestureDevice::kTouchscreen); |
| event.SetPositionInWidget(gfx::PointF(100, 100)); |
| if (type == WebInputEvent::Type::kGestureScrollUpdate) { |
| event.data.scroll_update.delta_x = delta_x; |
| event.data.scroll_update.delta_y = delta_y; |
| } else if (type == WebInputEvent::Type::kGestureScrollBegin) { |
| event.data.scroll_begin.delta_x_hint = delta_x; |
| event.data.scroll_begin.delta_y_hint = delta_y; |
| } |
| return WebCoalescedInputEvent(event, ui::LatencyInfo()); |
| } |
| |
| unsigned NumObjectsNeedingLayout() { |
| bool is_partial = false; |
| unsigned num_objects_need_layout = 0; |
| unsigned total_objects = 0; |
| GetDocument().View()->CountObjectsNeedingLayout(num_objects_need_layout, |
| total_objects, is_partial); |
| return num_objects_need_layout; |
| } |
| |
| protected: |
| protected: |
| RuntimeEnabledFeaturesTestHelpers::ScopedScrollUnification |
| scroll_unification_enabled_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, ScrollingSimTest, testing::Bool()); |
| |
| // Pre-scroll-unification, ensures that ScrollBegin and ScrollUpdate cause |
| // layout and ScrollEnd does not. Post unification, Blink will not handle these |
| // events but ensure that a unification main-thread-hit-test does cause layout. |
| TEST_P(ScrollingSimTest, ScrollLayoutTriggers) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <style> |
| #box { |
| position: absolute; |
| } |
| body { |
| height: 5000px; |
| } |
| </style> |
| <div id='box'></div> |
| )HTML"); |
| Compositor().BeginFrame(); |
| ASSERT_EQ(0u, NumObjectsNeedingLayout()); |
| |
| Element* box = GetDocument().getElementById("box"); |
| if (RuntimeEnabledFeatures::ScrollUnificationEnabled()) { |
| // Dirty the layout |
| box->setAttribute(html_names::kStyleAttr, "height: 10px"); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| ASSERT_NE(NumObjectsNeedingLayout(), 0u); |
| |
| // The hit test (which may be performed by a scroll begin) should cause a |
| // layout to occur. |
| WebView().MainFrameWidget()->HitTestResultAt(gfx::PointF(10, 10)); |
| EXPECT_EQ(NumObjectsNeedingLayout(), 0u); |
| |
| } else { |
| // ScrollBegin should trigger a layout. |
| { |
| // Dirty the layout |
| box->setAttribute(html_names::kStyleAttr, "height: 10px"); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| ASSERT_NE(NumObjectsNeedingLayout(), 0u); |
| |
| WebView().MainFrameWidget()->HandleInputEvent(GenerateGestureEvent( |
| WebInputEvent::Type::kGestureScrollBegin, 0, 10)); |
| EXPECT_EQ(NumObjectsNeedingLayout(), 0u); |
| } |
| |
| // ScrollUpdate should trigger a layout. |
| { |
| // Dirty the layout |
| box->setAttribute(html_names::kStyleAttr, "height: 11px"); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| ASSERT_NE(NumObjectsNeedingLayout(), 0u); |
| |
| WebView().MainFrameWidget()->HandleInputEvent(GenerateGestureEvent( |
| WebInputEvent::Type::kGestureScrollUpdate, 0, 10)); |
| EXPECT_EQ(NumObjectsNeedingLayout(), 0u); |
| } |
| |
| // ScrollEnd shouldn't trigger a layout. |
| { |
| // Dirty the layout |
| box->setAttribute(html_names::kStyleAttr, "height: 12px"); |
| GetDocument().UpdateStyleAndLayoutTree(); |
| ASSERT_NE(NumObjectsNeedingLayout(), 0u); |
| |
| WebView().MainFrameWidget()->HandleInputEvent( |
| GenerateGestureEvent(WebInputEvent::Type::kGestureScrollEnd, 0, 0)); |
| EXPECT_NE(NumObjectsNeedingLayout(), 0u); |
| } |
| } |
| } |
| |
| class ScrollingTestWithAcceleratedContext : public ScrollingTest { |
| protected: |
| void SetUp() override { |
| auto factory = [](FakeGLES2Interface* gl, bool* gpu_compositing_disabled) |
| -> std::unique_ptr<WebGraphicsContext3DProvider> { |
| *gpu_compositing_disabled = false; |
| gl->SetIsContextLost(false); |
| return std::make_unique<FakeWebGraphicsContext3DProvider>(gl); |
| }; |
| SharedGpuContext::SetContextProviderFactoryForTesting( |
| WTF::BindRepeating(factory, WTF::Unretained(&gl_))); |
| ScrollingTest::SetUp(); |
| } |
| |
| void TearDown() override { |
| SharedGpuContext::ResetForTesting(); |
| ScrollingTest::TearDown(); |
| } |
| |
| private: |
| FakeGLES2Interface gl_; |
| }; |
| |
| INSTANTIATE_PAINT_TEST_SUITE_P(ScrollingTestWithAcceleratedContext); |
| |
| TEST_P(ScrollingTestWithAcceleratedContext, CanvasTouchActionRects) { |
| LoadHTML(R"HTML( |
| <canvas id="canvas" style="touch-action: none; will-change: transform;"> |
| <script> |
| var canvas = document.getElementById("canvas"); |
| var ctx = canvas.getContext("2d"); |
| canvas.width = 400; |
| canvas.height = 400; |
| ctx.fillStyle = 'lightgrey'; |
| ctx.fillRect(0, 0, 400, 400); |
| </script> |
| )HTML"); |
| ForceFullCompositingUpdate(); |
| |
| const auto* cc_layer = LayerByDOMElementId("canvas"); |
| cc::Region region = cc_layer->touch_action_region().GetRegionForTouchAction( |
| TouchAction::kNone); |
| EXPECT_EQ(region.bounds(), gfx::Rect(0, 0, 400, 400)); |
| } |
| |
| } // namespace blink |