// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "third_party/blink/renderer/core/paint/pre_paint_tree_walk.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/renderer/core/dom/events/event_listener.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/layout/layout_tree_as_text.h" #include "third_party/blink/renderer/core/layout/layout_view.h" #include "third_party/blink/renderer/core/paint/object_paint_properties.h" #include "third_party/blink/renderer/core/paint/paint_controller_paint_test.h" #include "third_party/blink/renderer/core/paint/paint_layer.h" #include "third_party/blink/renderer/core/paint/paint_property_tree_printer.h" #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h" #include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h" #include "third_party/blink/renderer/platform/graphics/paint/scroll_paint_property_node.h" #include "third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h" #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" #include "third_party/blink/renderer/platform/wtf/hash_map.h" namespace blink { class PrePaintTreeWalkTest : public PaintControllerPaintTest { public: const TransformPaintPropertyNode* FramePreTranslation() { return GetDocument() .View() ->GetLayoutView() ->FirstFragment() .PaintProperties() ->PaintOffsetTranslation(); } const TransformPaintPropertyNode* FrameScrollTranslation() { return GetDocument() .View() ->GetLayoutView() ->FirstFragment() .PaintProperties() ->ScrollTranslation(); } protected: PaintLayer* GetPaintLayerByElementId(const char* id) { return ToLayoutBoxModelObject(GetLayoutObjectByElementId(id))->Layer(); } private: void SetUp() override { RenderingTest::SetUp(); EnableCompositing(); } }; INSTANTIATE_PAINT_TEST_CASE_P(PrePaintTreeWalkTest); TEST_P(PrePaintTreeWalkTest, PropertyTreesRebuiltWithBorderInvalidation) { SetBodyInnerHTML(R"HTML(
)HTML"); auto* transformed_element = GetDocument().getElementById("transformed"); const auto* transformed_properties = transformed_element->GetLayoutObject()->FirstFragment().PaintProperties(); EXPECT_EQ(TransformationMatrix().Translate(100, 100), transformed_properties->Transform()->Matrix()); // Artifically change the transform node. const_cast(transformed_properties)->ClearTransform(); EXPECT_EQ(nullptr, transformed_properties->Transform()); // Cause a paint invalidation. transformed_element->setAttribute(html_names::kClassAttr, "border"); UpdateAllLifecyclePhasesForTest(); // Should have changed back. EXPECT_EQ(TransformationMatrix().Translate(100, 100), transformed_properties->Transform()->Matrix()); } TEST_P(PrePaintTreeWalkTest, PropertyTreesRebuiltWithFrameScroll) { SetBodyInnerHTML(""); EXPECT_EQ(TransformationMatrix().Translate(0, 0), FrameScrollTranslation()->Matrix()); // Cause a scroll invalidation and ensure the translation is updated. GetDocument().domWindow()->scrollTo(0, 100); UpdateAllLifecyclePhasesForTest(); EXPECT_EQ(TransformationMatrix().Translate(0, -100), FrameScrollTranslation()->Matrix()); } TEST_P(PrePaintTreeWalkTest, PropertyTreesRebuiltWithCSSTransformInvalidation) { SetBodyInnerHTML(R"HTML(
)HTML"); auto* transformed_element = GetDocument().getElementById("transformed"); const auto* transformed_properties = transformed_element->GetLayoutObject()->FirstFragment().PaintProperties(); EXPECT_EQ(TransformationMatrix().Translate(100, 100), transformed_properties->Transform()->Matrix()); // Invalidate the CSS transform property. transformed_element->setAttribute(html_names::kClassAttr, "transformB"); UpdateAllLifecyclePhasesForTest(); // The transform should have changed. EXPECT_EQ(TransformationMatrix().Translate(200, 200), transformed_properties->Transform()->Matrix()); } TEST_P(PrePaintTreeWalkTest, PropertyTreesRebuiltWithOpacityInvalidation) { SetBodyInnerHTML(R"HTML(
)HTML"); auto* transparent_element = GetDocument().getElementById("transparent"); const auto* transparent_properties = transparent_element->GetLayoutObject()->FirstFragment().PaintProperties(); EXPECT_EQ(0.9f, transparent_properties->Effect()->Opacity()); // Invalidate the opacity property. transparent_element->setAttribute(html_names::kClassAttr, "opacityB"); UpdateAllLifecyclePhasesForTest(); // The opacity should have changed. EXPECT_EQ(0.4f, transparent_properties->Effect()->Opacity()); } TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChange) { SetBodyInnerHTML(R"HTML(
content
)HTML"); auto* parent = GetDocument().getElementById("parent"); auto* child = GetDocument().getElementById("child"); auto* child_paint_layer = ToLayoutBoxModelObject(child->GetLayoutObject())->Layer(); EXPECT_FALSE(child_paint_layer->NeedsRepaint()); EXPECT_FALSE(child_paint_layer->NeedsPaintPhaseFloat()); parent->setAttribute(html_names::kClassAttr, "clip"); GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(); EXPECT_TRUE(child_paint_layer->NeedsRepaint()); } TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChange2DTransform) { SetBodyInnerHTML(R"HTML(
content
)HTML"); auto* parent = GetDocument().getElementById("parent"); auto* child = GetDocument().getElementById("child"); auto* child_paint_layer = ToLayoutBoxModelObject(child->GetLayoutObject())->Layer(); EXPECT_FALSE(child_paint_layer->NeedsRepaint()); EXPECT_FALSE(child_paint_layer->NeedsPaintPhaseFloat()); parent->setAttribute(html_names::kClassAttr, "clip"); GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(); EXPECT_TRUE(child_paint_layer->NeedsRepaint()); } TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChangePosAbs) { SetBodyInnerHTML(R"HTML(
content
)HTML"); auto* parent = GetDocument().getElementById("parent"); auto* child = GetDocument().getElementById("child"); auto* child_paint_layer = ToLayoutBoxModelObject(child->GetLayoutObject())->Layer(); EXPECT_FALSE(child_paint_layer->NeedsRepaint()); EXPECT_FALSE(child_paint_layer->NeedsPaintPhaseFloat()); // This changes clips for absolute-positioned descendants of "child" but not // normal-position ones, which are already clipped to 50x50. parent->setAttribute(html_names::kClassAttr, "clip"); GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(); EXPECT_TRUE(child_paint_layer->NeedsRepaint()); } TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChangePosFixed) { SetBodyInnerHTML(R"HTML(
content
)HTML"); auto* parent = GetDocument().getElementById("parent"); auto* child = GetDocument().getElementById("child"); auto* child_paint_layer = ToLayoutBoxModelObject(child->GetLayoutObject())->Layer(); EXPECT_FALSE(child_paint_layer->NeedsRepaint()); EXPECT_FALSE(child_paint_layer->NeedsPaintPhaseFloat()); // This changes clips for absolute-positioned descendants of "child" but not // normal-position ones, which are already clipped to 50x50. parent->setAttribute(html_names::kClassAttr, "clip"); GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(); EXPECT_TRUE(child_paint_layer->NeedsRepaint()); } TEST_P(PrePaintTreeWalkTest, VisualRectClipForceSubtree) { SetBodyInnerHTML(R"HTML(
)HTML"); auto* grandchild = GetLayoutObjectByElementId("grandchild"); GetDocument().getElementById("parent")->removeAttribute("style"); UpdateAllLifecyclePhasesForTest(); EXPECT_EQ(200, grandchild->FirstFragment().VisualRect().Height()); } TEST_P(PrePaintTreeWalkTest, ClipChangeHasRadius) { SetBodyInnerHTML(R"HTML(
)HTML"); auto* target = GetDocument().getElementById("target"); auto* target_object = ToLayoutBoxModelObject(target->GetLayoutObject()); target->setAttribute(html_names::kStyleAttr, "border-radius: 5px"); GetDocument().View()->UpdateAllLifecyclePhasesExceptPaint(); EXPECT_TRUE(target_object->Layer()->NeedsRepaint()); // And should not trigger any assert failure. UpdateAllLifecyclePhasesForTest(); } namespace { class PrePaintTreeWalkMockEventListener final : public EventListener { public: PrePaintTreeWalkMockEventListener() : EventListener(kCPPEventListenerType) {} bool operator==(const EventListener& other) const final { return this == &other; } void Invoke(ExecutionContext*, Event*) final {} }; } // namespace TEST_P(PrePaintTreeWalkTest, InsideBlockingTouchEventHandlerUpdate) { ScopedPaintTouchActionRectsForTest enable_paint_touch_action_rects(true); SetBodyInnerHTML(R"HTML(
)HTML"); UpdateAllLifecyclePhasesForTest(); auto& ancestor = *GetLayoutObjectByElementId("ancestor"); auto& handler = *GetLayoutObjectByElementId("handler"); auto& descendant = *GetLayoutObjectByElementId("descendant"); EXPECT_FALSE(ancestor.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(handler.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(ancestor.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(handler.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(ancestor.InsideBlockingTouchEventHandler()); EXPECT_FALSE(handler.InsideBlockingTouchEventHandler()); EXPECT_FALSE(descendant.InsideBlockingTouchEventHandler()); PrePaintTreeWalkMockEventListener* callback = new PrePaintTreeWalkMockEventListener(); auto* handler_element = GetDocument().getElementById("handler"); handler_element->addEventListener(event_type_names::kTouchstart, callback); EXPECT_FALSE(ancestor.EffectiveWhitelistedTouchActionChanged()); EXPECT_TRUE(handler.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.EffectiveWhitelistedTouchActionChanged()); EXPECT_TRUE(ancestor.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(handler.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.DescendantEffectiveWhitelistedTouchActionChanged()); UpdateAllLifecyclePhasesForTest(); EXPECT_FALSE(ancestor.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(handler.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(ancestor.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(handler.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(ancestor.InsideBlockingTouchEventHandler()); EXPECT_TRUE(handler.InsideBlockingTouchEventHandler()); EXPECT_TRUE(descendant.InsideBlockingTouchEventHandler()); } TEST_P(PrePaintTreeWalkTest, EffectiveTouchActionStyleUpdate) { ScopedPaintTouchActionRectsForTest enable_paint_touch_action_rects(true); SetBodyInnerHTML(R"HTML(
)HTML"); UpdateAllLifecyclePhasesForTest(); auto& ancestor = *GetLayoutObjectByElementId("ancestor"); auto& touchaction = *GetLayoutObjectByElementId("touchaction"); auto& descendant = *GetLayoutObjectByElementId("descendant"); EXPECT_FALSE(ancestor.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(touchaction.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(ancestor.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(touchaction.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.DescendantEffectiveWhitelistedTouchActionChanged()); GetDocument() .getElementById("touchaction") ->setAttribute(html_names::kClassAttr, "touchaction"); GetDocument().View()->UpdateLifecycleToLayoutClean(); EXPECT_FALSE(ancestor.EffectiveWhitelistedTouchActionChanged()); EXPECT_TRUE(touchaction.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.EffectiveWhitelistedTouchActionChanged()); EXPECT_TRUE(ancestor.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(touchaction.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.DescendantEffectiveWhitelistedTouchActionChanged()); UpdateAllLifecyclePhasesForTest(); EXPECT_FALSE(ancestor.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(touchaction.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.EffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(ancestor.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(touchaction.DescendantEffectiveWhitelistedTouchActionChanged()); EXPECT_FALSE(descendant.DescendantEffectiveWhitelistedTouchActionChanged()); } TEST_P(PrePaintTreeWalkTest, ClipChangesDoNotCauseVisualRectUpdates) { ScopedPaintTouchActionRectsForTest enable_paint_touch_action_rects(true); SetBodyInnerHTML(R"HTML(
)HTML"); GetDocument().getElementById("parent")->setAttribute(html_names::kStyleAttr, "border-radius: 5px"); UpdateAllLifecyclePhasesForTest(); auto& parent = *GetLayoutObjectByElementId("parent"); auto& child = *GetLayoutObjectByElementId("child"); // Cause the child to go down the prepaint path but without on its own // requiring a tree builder context. child.SetShouldCheckForPaintInvalidationWithoutGeometryChange(); EXPECT_EQ(100, parent.FirstFragment().VisualRect().Width()); EXPECT_EQ(100, parent.FirstFragment().VisualRect().Height()); EXPECT_EQ(100, child.FirstFragment().VisualRect().Width()); EXPECT_EQ(200, child.FirstFragment().VisualRect().Height()); // Cause the child clip to change without changing paint property tree // topology. GetDocument().getElementById("parent")->setAttribute(html_names::kStyleAttr, "border-radius: 6px"); UpdateAllLifecyclePhasesForTest(); EXPECT_EQ(100, parent.FirstFragment().VisualRect().Width()); EXPECT_EQ(100, parent.FirstFragment().VisualRect().Height()); EXPECT_EQ(100, child.FirstFragment().VisualRect().Width()); EXPECT_EQ(200, child.FirstFragment().VisualRect().Height()); } } // namespace blink