// 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(
)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(
)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(
)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(
)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