// 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 "base/test/scoped_feature_list.h"
#include "cc/base/features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/dom/events/native_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();
}
private:
void SetUp() override {
EnableCompositing();
RenderingTest::SetUp();
}
};
INSTANTIATE_PAINT_TEST_SUITE_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(FloatSize(100, 100),
transformed_properties->Transform()->Translation2D());
// 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(FloatSize(100, 100),
transformed_properties->Transform()->Translation2D());
}
TEST_P(PrePaintTreeWalkTest, PropertyTreesRebuiltWithFrameScroll) {
SetBodyInnerHTML("");
EXPECT_TRUE(FrameScrollTranslation()->IsIdentity());
// Cause a scroll invalidation and ensure the translation is updated.
GetDocument().domWindow()->scrollTo(0, 100);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ(FloatSize(0, -100), FrameScrollTranslation()->Translation2D());
}
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(FloatSize(100, 100),
transformed_properties->Transform()->Translation2D());
// Invalidate the CSS transform property.
transformed_element->setAttribute(html_names::kClassAttr, "transformB");
UpdateAllLifecyclePhasesForTest();
// The transform should have changed.
EXPECT_EQ(FloatSize(200, 200),
transformed_properties->Transform()->Translation2D());
}
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(
)HTML");
auto* parent = GetDocument().getElementById("parent");
auto* child_paint_layer = GetPaintLayerByElementId("child");
EXPECT_FALSE(child_paint_layer->SelfNeedsRepaint());
EXPECT_FALSE(child_paint_layer->NeedsPaintPhaseFloat());
parent->setAttribute(html_names::kClassAttr, "clip");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(child_paint_layer->SelfNeedsRepaint());
}
TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChange2DTransform) {
SetBodyInnerHTML(R"HTML(
)HTML");
auto* parent = GetDocument().getElementById("parent");
auto* child_paint_layer = GetPaintLayerByElementId("child");
EXPECT_FALSE(child_paint_layer->SelfNeedsRepaint());
EXPECT_FALSE(child_paint_layer->NeedsPaintPhaseFloat());
parent->setAttribute(html_names::kClassAttr, "clip");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(child_paint_layer->SelfNeedsRepaint());
}
TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChangePosAbs) {
SetBodyInnerHTML(R"HTML(
)HTML");
auto* parent = GetDocument().getElementById("parent");
auto* child_paint_layer = GetPaintLayerByElementId("child");
EXPECT_FALSE(child_paint_layer->SelfNeedsRepaint());
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");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(child_paint_layer->SelfNeedsRepaint());
}
TEST_P(PrePaintTreeWalkTest, ClearSubsequenceCachingClipChangePosFixed) {
SetBodyInnerHTML(R"HTML(
)HTML");
auto* parent = GetDocument().getElementById("parent");
auto* child_paint_layer = GetPaintLayerByElementId("child");
EXPECT_FALSE(child_paint_layer->SelfNeedsRepaint());
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");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(child_paint_layer->SelfNeedsRepaint());
}
TEST_P(PrePaintTreeWalkTest, ClipChangeRepaintsDescendants) {
SetBodyInnerHTML(R"HTML(
)HTML");
GetDocument().getElementById("parent")->removeAttribute("style");
UpdateAllLifecyclePhasesExceptPaint();
auto* paint_layer = GetPaintLayerByElementId("greatgrandchild");
EXPECT_TRUE(paint_layer->SelfNeedsRepaint());
}
TEST_P(PrePaintTreeWalkTest, ClipChangeHasRadius) {
SetBodyInnerHTML(R"HTML(
)HTML");
auto* target = GetDocument().getElementById("target");
auto* target_object = To(target->GetLayoutObject());
target->setAttribute(html_names::kStyleAttr, "border-radius: 5px");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(target_object->Layer()->SelfNeedsRepaint());
// And should not trigger any assert failure.
UpdateAllLifecyclePhasesForTest();
}
namespace {
class PrePaintTreeWalkMockEventListener final : public NativeEventListener {
public:
void Invoke(ExecutionContext*, Event*) final {}
};
} // namespace
TEST_P(PrePaintTreeWalkTest, InsideBlockingTouchEventHandlerUpdate) {
SetBodyInnerHTML(R"HTML(
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& ancestor = *GetLayoutObjectByElementId("ancestor");
auto& handler = *GetLayoutObjectByElementId("handler");
auto& descendant = *GetLayoutObjectByElementId("descendant");
EXPECT_FALSE(ancestor.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(handler.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(ancestor.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(handler.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(ancestor.InsideBlockingTouchEventHandler());
EXPECT_FALSE(handler.InsideBlockingTouchEventHandler());
EXPECT_FALSE(descendant.InsideBlockingTouchEventHandler());
PrePaintTreeWalkMockEventListener* callback =
MakeGarbageCollected();
auto* handler_element = GetDocument().getElementById("handler");
handler_element->addEventListener(event_type_names::kTouchstart, callback);
EXPECT_FALSE(ancestor.EffectiveAllowedTouchActionChanged());
EXPECT_TRUE(handler.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.EffectiveAllowedTouchActionChanged());
EXPECT_TRUE(ancestor.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(handler.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.DescendantEffectiveAllowedTouchActionChanged());
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(ancestor.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(handler.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(ancestor.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(handler.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(ancestor.InsideBlockingTouchEventHandler());
EXPECT_TRUE(handler.InsideBlockingTouchEventHandler());
EXPECT_TRUE(descendant.InsideBlockingTouchEventHandler());
}
TEST_P(PrePaintTreeWalkTest, EffectiveTouchActionStyleUpdate) {
SetBodyInnerHTML(R"HTML(
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& ancestor = *GetLayoutObjectByElementId("ancestor");
auto& touchaction = *GetLayoutObjectByElementId("touchaction");
auto& descendant = *GetLayoutObjectByElementId("descendant");
EXPECT_FALSE(ancestor.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(touchaction.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(ancestor.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(touchaction.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.DescendantEffectiveAllowedTouchActionChanged());
GetDocument()
.getElementById("touchaction")
->setAttribute(html_names::kClassAttr, "touchaction");
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
EXPECT_FALSE(ancestor.EffectiveAllowedTouchActionChanged());
EXPECT_TRUE(touchaction.EffectiveAllowedTouchActionChanged());
EXPECT_TRUE(descendant.EffectiveAllowedTouchActionChanged());
EXPECT_TRUE(ancestor.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_TRUE(touchaction.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.DescendantEffectiveAllowedTouchActionChanged());
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(ancestor.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(touchaction.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.EffectiveAllowedTouchActionChanged());
EXPECT_FALSE(ancestor.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(touchaction.DescendantEffectiveAllowedTouchActionChanged());
EXPECT_FALSE(descendant.DescendantEffectiveAllowedTouchActionChanged());
}
TEST_P(PrePaintTreeWalkTest, InsideBlockingWheelEventHandlerUpdate) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(::features::kWheelEventRegions);
SetBodyInnerHTML(R"HTML(
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& ancestor = *GetLayoutObjectByElementId("ancestor");
auto& handler = *GetLayoutObjectByElementId("handler");
auto& descendant = *GetLayoutObjectByElementId("descendant");
EXPECT_FALSE(ancestor.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(handler.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(descendant.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(ancestor.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(handler.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(descendant.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(ancestor.InsideBlockingWheelEventHandler());
EXPECT_FALSE(handler.InsideBlockingWheelEventHandler());
EXPECT_FALSE(descendant.InsideBlockingWheelEventHandler());
PrePaintTreeWalkMockEventListener* callback =
MakeGarbageCollected();
auto* handler_element = GetDocument().getElementById("handler");
handler_element->addEventListener(event_type_names::kWheel, callback);
EXPECT_FALSE(ancestor.BlockingWheelEventHandlerChanged());
EXPECT_TRUE(handler.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(descendant.BlockingWheelEventHandlerChanged());
EXPECT_TRUE(ancestor.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(handler.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(descendant.DescendantBlockingWheelEventHandlerChanged());
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(ancestor.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(handler.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(descendant.BlockingWheelEventHandlerChanged());
EXPECT_FALSE(ancestor.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(handler.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(descendant.DescendantBlockingWheelEventHandlerChanged());
EXPECT_FALSE(ancestor.InsideBlockingWheelEventHandler());
EXPECT_TRUE(handler.InsideBlockingWheelEventHandler());
EXPECT_TRUE(descendant.InsideBlockingWheelEventHandler());
}
} // namespace blink