blob: 542f008a395f4448edf43c47cb0630a8e7907031 [file] [log] [blame]
// 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/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();
}
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(
<style>
body { margin: 0; }
#transformed { transform: translate(100px, 100px); }
.border { border: 10px solid black; }
</style>
<div id='transformed'></div>
)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<ObjectPaintProperties*>(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("<style> body { height: 10000px; } </style>");
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(
<style>
.transformA { transform: translate(100px, 100px); }
.transformB { transform: translate(200px, 200px); }
#transformed { will-change: transform; }
</style>
<div id='transformed' class='transformA'></div>
)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(
<style>
.opacityA { opacity: 0.9; }
.opacityB { opacity: 0.4; }
</style>
<div id='transparent' class='opacityA'></div>
)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(
<style>
.clip { overflow: hidden }
</style>
<div id='parent' style='transform: translateZ(0); width: 100px;
height: 100px;'>
<div id='child' style='isolation: isolate'>
content
</div>
</div>
)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(
<style>
.clip { overflow: hidden }
</style>
<div id='parent' style='transform: translateX(0); width: 100px;
height: 100px;'>
<div id='child' style='isolation: isolate'>
content
</div>
</div>
)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(
<style>
.clip { overflow: hidden }
</style>
<div id='parent' style='transform: translateZ(0); width: 100px;
height: 100px; position: absolute'>
<div id='child' style='overflow: hidden; position: relative;
z-index: 0; width: 50px; height: 50px'>
content
</div>
</div>
)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(
<style>
.clip { overflow: hidden }
</style>
<div id='parent' style='transform: translateZ(0); width: 100px;
height: 100px; trans'>
<div id='child' style='overflow: hidden; z-index: 0;
position: absolute; width: 50px; height: 50px'>
content
</div>
</div>
)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(
<style>
#parent { height: 75px; position: relative; width: 100px; }
</style>
<div id='parent' style='height: 100px;'>
<div id='child' style='overflow: hidden; width: 100%; height: 100%;
position: relative'>
<div>
<div id='grandchild' style='width: 50px; height: 200px; '>
</div>
</div>
</div>
</div>
)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(
<style>
#target {
position: absolute;
z-index: 0;
overflow: hidden;
width: 50px;
height: 50px;
}
</style>
<div id='target'></div>
)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 NativeEventListener {
public:
void Invoke(ExecutionContext*, Event*) final {}
};
} // namespace
TEST_P(PrePaintTreeWalkTest, InsideBlockingTouchEventHandlerUpdate) {
ScopedPaintTouchActionRectsForTest enable_paint_touch_action_rects(true);
SetBodyInnerHTML(R"HTML(
<div id='ancestor' style='width: 100px; height: 100px;'>
<div id='handler' style='width: 100px; height: 100px;'>
<div id='descendant' style='width: 100px; height: 100px;'>
</div>
</div>
</div>
)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 =
MakeGarbageCollected<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(
<style> .touchaction { touch-action: none; } </style>
<div id='ancestor' style='width: 100px; height: 100px;'>
<div id='touchaction' style='width: 100px; height: 100px;'>
<div id='descendant' style='width: 100px; height: 100px;'>
</div>
</div>
</div>
)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(
<style> #parent { width: 100px; height: 100px; overflow: hidden; } </style>
<div id='parent'>
<div id='child' style='width: 100px; height: 200px;'>
</div>
</div>
)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