blob: 8d90be489a77c3730028e76d5e35319ba14215cb [file] [log] [blame]
/*
* 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