blob: 4154b126e2301aba2eb1b427572745d8ccdc4c8c [file] [log] [blame]
// Copyright 2018 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/layout/layout_shift_tracker.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/renderer/core/dom/dom_token_list.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.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/forms/html_select_element.h"
#include "third_party/blink/renderer/core/svg_names.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.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/core/timing/dom_window_performance.h"
#include "third_party/blink/renderer/core/timing/layout_shift.h"
#include "third_party/blink/renderer/core/timing/window_performance.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
namespace blink {
class LayoutShiftTrackerTest : public RenderingTest {
protected:
void SetUp() override {
EnableCompositing();
RenderingTest::SetUp();
}
LocalFrameView& GetFrameView() { return *GetFrame().View(); }
LayoutShiftTracker& GetLayoutShiftTracker() {
return GetFrameView().GetLayoutShiftTracker();
}
void SimulateInput() {
GetLayoutShiftTracker().NotifyInput(WebMouseEvent(
WebInputEvent::Type::kMouseDown, gfx::PointF(), gfx::PointF(),
WebPointerProperties::Button::kLeft, 0,
WebInputEvent::Modifiers::kLeftButtonDown, base::TimeTicks::Now()));
}
};
TEST_F(LayoutShiftTrackerTest, IgnoreAfterInput) {
SetBodyInnerHTML(R"HTML(
<style>
#j { position: relative; width: 300px; height: 100px; background: blue; }
</style>
<div id='j'></div>
)HTML");
GetDocument().getElementById("j")->setAttribute(html_names::kStyleAttr,
AtomicString("top: 60px"));
SimulateInput();
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ(0.0, GetLayoutShiftTracker().Score());
EXPECT_TRUE(GetLayoutShiftTracker().ObservedInputOrScroll());
EXPECT_TRUE(GetLayoutShiftTracker()
.MostRecentInputTimestamp()
.since_origin()
.InSecondsF() > 0.0);
}
TEST_F(LayoutShiftTrackerTest, CompositedShiftBeforeFirstPaint) {
// Tests that we don't crash if a new layer shifts during a second compositing
// update before prepaint sets up property tree state. See crbug.com/881735
// (which invokes UpdateLifecycleToCompositingCleanPlusScrolling through
// accessibilityController.accessibleElementById).
SetBodyInnerHTML(R"HTML(
<style>
.hide { display: none; }
.tr { will-change: transform; }
body { margin: 0; }
div { height: 100px; background: blue; }
</style>
<div id="container">
<div id="A">A</div>
<div id="B" class="tr hide">B</div>
</div>
)HTML");
GetDocument().getElementById("B")->setAttribute(html_names::kClassAttr,
AtomicString("tr"));
GetFrameView().UpdateLifecycleToCompositingCleanPlusScrolling(
DocumentUpdateReason::kTest);
GetDocument().getElementById("A")->setAttribute(html_names::kClassAttr,
AtomicString("hide"));
UpdateAllLifecyclePhasesForTest();
}
TEST_F(LayoutShiftTrackerTest, IgnoreSVG) {
SetBodyInnerHTML(R"HTML(
<svg>
<circle cx="50" cy="50" r="40"
stroke="black" stroke-width="3" fill="red" />
</svg>
)HTML");
GetDocument().QuerySelector("circle")->setAttribute(svg_names::kCxAttr,
AtomicString("100"));
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
}
TEST_F(LayoutShiftTrackerTest, IgnoreAfterChangeEvent) {
SetBodyInnerHTML(R"HTML(
<style>
#j { position: relative; width: 300px; height: 100px; background: blue; }
</style>
<div id='j'></div>
<select id="sel" onchange="shift()">
<option value="0">0</option>
<option value="1">1</option>
</select>
)HTML");
auto* select = To<HTMLSelectElement>(GetDocument().getElementById("sel"));
DCHECK(select);
select->focus();
select->SelectOptionByPopup(1);
GetDocument().getElementById("j")->setAttribute(html_names::kStyleAttr,
AtomicString("top: 60px"));
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
}
class LayoutShiftTrackerSimTest : public SimTest {
protected:
void SetUp() override {
SimTest::SetUp();
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
}
};
TEST_F(LayoutShiftTrackerSimTest, SubframeWeighting) {
// TODO(crbug.com/943668): Test OOPIF path.
SimRequest main_resource("https://example.com/", "text/html");
SimRequest child_resource("https://example.com/sub.html", "text/html");
LoadURL("https://example.com/");
main_resource.Complete(R"HTML(
<style> #i { border: 0; position: absolute; left: 0; top: 0; } </style>
<iframe id=i width=400 height=300 src='sub.html'></iframe>
)HTML");
Compositor().BeginFrame();
test::RunPendingTasks();
child_resource.Complete(R"HTML(
<style>
#j { position: relative; width: 300px; height: 100px; background: blue; }
</style>
<div id='j'></div>
)HTML");
Compositor().BeginFrame();
test::RunPendingTasks();
WebLocalFrameImpl& child_frame =
To<WebLocalFrameImpl>(*MainFrame().FirstChild());
Element* div = child_frame.GetFrame()->GetDocument()->getElementById("j");
div->setAttribute(html_names::kStyleAttr, AtomicString("top: 60px"));
Compositor().BeginFrame();
test::RunPendingTasks();
// 300 * (100 + 60) * (60 / 400) / (default viewport size 800 * 600)
LayoutShiftTracker& layout_shift_tracker =
child_frame.GetFrameView()->GetLayoutShiftTracker();
EXPECT_FLOAT_EQ(0.4 * (60.0 / 400.0), layout_shift_tracker.Score());
EXPECT_FLOAT_EQ(0.1 * (60.0 / 400.0), layout_shift_tracker.WeightedScore());
// Move subframe halfway outside the viewport.
GetDocument().getElementById("i")->setAttribute(html_names::kStyleAttr,
AtomicString("left: 600px"));
div->removeAttribute(html_names::kStyleAttr);
Compositor().BeginFrame();
test::RunPendingTasks();
EXPECT_FLOAT_EQ(0.8 * (60.0 / 400.0), layout_shift_tracker.Score());
EXPECT_FLOAT_EQ(0.15 * (60.0 / 400.0), layout_shift_tracker.WeightedScore());
}
TEST_F(LayoutShiftTrackerSimTest, ViewportSizeChange) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete(R"HTML(
<style>
body { margin: 0; }
.square {
display: inline-block;
position: relative;
width: 300px;
height: 300px;
background:yellow;
}
</style>
<div class='square'></div>
<div class='square'></div>
)HTML");
Compositor().BeginFrame();
test::RunPendingTasks();
// Resize the viewport, making it 400px wide. This should cause the second div
// to change position during block layout flow. Since it was the result of a
// viewport size change, this position change should not affect the score.
WebView().MainFrameViewWidget()->Resize(gfx::Size(400, 600));
Compositor().BeginFrame();
test::RunPendingTasks();
LayoutShiftTracker& layout_shift_tracker =
MainFrame().GetFrameView()->GetLayoutShiftTracker();
EXPECT_FLOAT_EQ(0.0, layout_shift_tracker.Score());
}
class LayoutShiftTrackerPointerdownTest : public LayoutShiftTrackerSimTest {
protected:
void RunTest(WebInputEvent::Type completion_type, bool expect_exclusion);
};
void LayoutShiftTrackerPointerdownTest::RunTest(
WebInputEvent::Type completion_type,
bool expect_exclusion) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete(R"HTML(
<style>
body { margin: 0; height: 1500px; }
#box {
left: 0px;
top: 0px;
width: 400px;
height: 600px;
background: yellow;
position: relative;
}
</style>
<div id="box"></div>
<script>
box.addEventListener("pointerdown", (e) => {
box.style.top = "100px";
e.preventDefault();
});
</script>
)HTML");
Compositor().BeginFrame();
test::RunPendingTasks();
WebPointerProperties pointer_properties = WebPointerProperties(
1 /* PointerId */, WebPointerProperties::PointerType::kTouch,
WebPointerProperties::Button::kLeft);
WebPointerEvent event1(WebInputEvent::Type::kPointerDown, pointer_properties,
5, 5);
WebPointerEvent event2(completion_type, pointer_properties, 5, 5);
// Coordinates inside #box.
event1.SetPositionInWidget(50, 150);
event2.SetPositionInWidget(50, 160);
WebView().MainFrameWidget()->HandleInputEvent(
WebCoalescedInputEvent(event1, ui::LatencyInfo()));
Compositor().BeginFrame();
test::RunPendingTasks();
WindowPerformance& perf = *DOMWindowPerformance::performance(Window());
auto& tracker = MainFrame().GetFrameView()->GetLayoutShiftTracker();
EXPECT_EQ(0u, perf.getBufferedEntriesByType("layout-shift").size());
EXPECT_FLOAT_EQ(0.0, tracker.Score());
WebView().MainFrameWidget()->HandleInputEvent(
WebCoalescedInputEvent(event2, ui::LatencyInfo()));
// region fraction 50%, distance fraction 1/8
const double expected_shift = 0.5 * 0.125;
auto entries = perf.getBufferedEntriesByType("layout-shift");
EXPECT_EQ(1u, entries.size());
LayoutShift* shift = static_cast<LayoutShift*>(entries.front().Get());
EXPECT_EQ(expect_exclusion, shift->hadRecentInput());
EXPECT_FLOAT_EQ(expected_shift, shift->value());
EXPECT_FLOAT_EQ(expect_exclusion ? 0.0 : expected_shift, tracker.Score());
}
TEST_F(LayoutShiftTrackerPointerdownTest, PointerdownBecomesTap) {
RunTest(WebInputEvent::Type::kPointerUp, true /* expect_exclusion */);
}
TEST_F(LayoutShiftTrackerPointerdownTest, PointerdownCancelled) {
RunTest(WebInputEvent::Type::kPointerCancel, false /* expect_exclusion */);
}
TEST_F(LayoutShiftTrackerPointerdownTest, PointerdownBecomesScroll) {
RunTest(WebInputEvent::Type::kPointerCausedUaAction,
false /* expect_exclusion */);
}
TEST_F(LayoutShiftTrackerTest, StableCompositingChanges) {
SetBodyInnerHTML(R"HTML(
<style>
body { margin: 0; }
#outer {
margin-left: 50px;
margin-top: 50px;
width: 200px;
height: 200px;
background: #dde;
}
.tr {
will-change: transform;
}
.pl {
position: relative;
z-index: 0;
left: 0;
top: 0;
}
#inner {
display: inline-block;
width: 100px;
height: 100px;
background: #666;
margin-left: 50px;
margin-top: 50px;
}
</style>
<div id=outer><div id=inner></div></div>
)HTML");
Element* element = GetDocument().getElementById("outer");
size_t state = 0;
auto advance = [this, element, &state]() -> bool {
//
// Test each of the following transitions:
// - add/remove a PaintLayer
// - add/remove a cc::Layer when there is already a PaintLayer
// - add/remove a cc::Layer and a PaintLayer together
static const char* states[] = {"", "pl", "pl tr", "pl", "", "tr", ""};
element->setAttribute(html_names::kClassAttr, AtomicString(states[state]));
UpdateAllLifecyclePhasesForTest();
return ++state < sizeof states / sizeof *states;
};
while (advance()) {
}
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
}
TEST_F(LayoutShiftTrackerTest, CompositedOverflowExpansion) {
SetBodyInnerHTML(R"HTML(
<style>
html { will-change: transform; }
body { height: 2000px; margin: 0; }
#drop {
position: absolute;
width: 1px;
height: 1px;
left: -10000px;
top: -1000px;
}
.pl {
position: relative;
background: #ddd;
z-index: 0;
width: 290px;
height: 170px;
left: 25px;
top: 25px;
}
#comp {
position: relative;
width: 240px;
height: 120px;
background: #efe;
will-change: transform;
z-index: 0;
left: 25px;
top: 25px;
}
.sh {
top: 515px !important;
}
</style>
<div class="pl">
<div id="comp"></div>
</div>
<div id="drop" style="display: none"></div>
)HTML");
Element* drop = GetDocument().getElementById("drop");
drop->removeAttribute(html_names::kStyleAttr);
UpdateAllLifecyclePhasesForTest();
drop->setAttribute(html_names::kStyleAttr, AtomicString("display: none"));
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
Element* comp = GetDocument().getElementById("comp");
comp->setAttribute(html_names::kClassAttr, AtomicString("sh"));
drop->removeAttribute(html_names::kStyleAttr);
UpdateAllLifecyclePhasesForTest();
// old rect (240 * 120) / (800 * 600) = 0.06
// new rect, 50% clipped by viewport (240 * 60) / (800 * 600) = 0.03
// final score 0.06 + 0.03 = 0.09 * (490 move distance / 800)
EXPECT_FLOAT_EQ(0.09 * (490.0 / 800.0), GetLayoutShiftTracker().Score());
}
TEST_F(LayoutShiftTrackerTest, ContentVisibilityAutoFirstPaint) {
SetBodyInnerHTML(R"HTML(
<style>
.auto {
content-visibility: auto;
contain-intrinsic-size: 1px;
width: 100px;
}
</style>
<div id=target class=auto>
<div style="width: 100px; height: 100px; background: blue"></div>
</div>
)HTML");
auto* target = To<LayoutBox>(GetLayoutObjectByElementId("target"));
// Because it's on-screen on the first frame, #target renders at size
// 100x100 on the first frame, via a synchronous second layout, and there is
// no CLS impact.
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 100), target->Size());
}
TEST_F(LayoutShiftTrackerTest,
ContentVisibilityAutoOffscreenAfterScrollFirstPaint) {
SetBodyInnerHTML(R"HTML(
<style>
.auto {
content-visibility: auto;
contain-intrinsic-size: 1px;
width: 100px;
}
</style>
<div id=target class=auto style="position: relative; top: 100000px">
<div style="width: 100px; height: 100px; background: blue"></div>
</div>
)HTML");
auto* target = To<LayoutBox>(GetLayoutObjectByElementId("target"));
// #target starts offsceen, which doesn't count for CLS.
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 1), target->Size());
// In the next frame, we scroll it onto the screen, but it still doesn't
// count for CLS, and its subtree is not yet unskipped, because the
// intersection observation takes effect on the subsequent frame.
GetDocument().domWindow()->scrollTo(0, 100000);
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 1), target->Size());
// Now the subtree is unskipped, and #target renders at size 100x100.
// Nevertheless, there is no impact on CLS.
UpdateAllLifecyclePhasesForTest();
// Target's LayoutObject gets re-attached.
target = To<LayoutBox>(GetLayoutObjectByElementId("target"));
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 100), target->Size());
}
TEST_F(LayoutShiftTrackerTest, ContentVisibilityHiddenFirstPaint) {
SetBodyInnerHTML(R"HTML(
<style>
.auto {
content-visibility: hidden;
contain-intrinsic-size: 1px;
width: 100px;
}
</style>
<div id=target class=auto>
<div style="width: 100px; height: 100px; background: blue"></div>
</div>
)HTML");
auto* target = To<LayoutBox>(GetLayoutObjectByElementId("target"));
// Skipped subtrees don't cause CLS impact.
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 1), target->Size());
}
TEST_F(LayoutShiftTrackerTest, ContentVisibilityAutoResize) {
SetBodyInnerHTML(R"HTML(
<style>
.auto {
content-visibility: auto;
contain-intrinsic-size: 10px 3000px;
width: 100px;
}
.contained {
height: 100px;
background: blue;
}
</style>
<div class=auto><div class=contained></div></div>
<div class=auto id=target><div class=contained></div></div>
)HTML");
// Skipped subtrees don't cause CLS impact.
UpdateAllLifecyclePhasesForTest();
auto* target = To<LayoutBox>(GetLayoutObjectByElementId("target"));
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 100), target->Size());
}
TEST_F(LayoutShiftTrackerTest,
ContentVisibilityAutoOnscreenAndOffscreenAfterScrollFirstPaint) {
SetBodyInnerHTML(R"HTML(
<style>
.auto {
content-visibility: auto;
contain-intrinsic-size: 1px;
width: 100px;
}
</style>
<div id=onscreen class=auto>
<div style="width: 100px; height: 100px; background: blue"></div>
</div>
<div id=offscreen class=auto style="position: relative; top: 100000px">
<div style="width: 100px; height: 100px; background: blue"></div>
</div>
)HTML");
auto* offscreen = To<LayoutBox>(GetLayoutObjectByElementId("offscreen"));
auto* onscreen = To<LayoutBox>(GetLayoutObjectByElementId("onscreen"));
// #offscreen starts offsceen, which doesn't count for CLS.
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 1), offscreen->Size());
EXPECT_EQ(LayoutSize(100, 100), onscreen->Size());
// In the next frame, we scroll it onto the screen, but it still doesn't
// count for CLS, and its subtree is not yet unskipped, because the
// intersection observation takes effect on the subsequent frame.
GetDocument().domWindow()->scrollTo(0, 100000 + 100);
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 1), offscreen->Size());
EXPECT_EQ(LayoutSize(100, 100), onscreen->Size());
// Now the subtree is unskipped, and #offscreen renders at size 100x100.
// Nevertheless, there is no impact on CLS.
UpdateAllLifecyclePhasesForTest();
offscreen = To<LayoutBox>(GetLayoutObjectByElementId("offscreen"));
onscreen = To<LayoutBox>(GetLayoutObjectByElementId("onscreen"));
// Target's LayoutObject gets re-attached.
offscreen = To<LayoutBox>(GetLayoutObjectByElementId("offscreen"));
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 100), offscreen->Size());
EXPECT_EQ(LayoutSize(100, 1), onscreen->Size());
// Move |offscreen| (which is visible and unlocked now), for which we should
// report layout shift.
To<Element>(offscreen->GetNode())
->setAttribute(html_names::kStyleAttr,
"position: relative; top: 100100px");
UpdateAllLifecyclePhasesForTest();
auto score = GetLayoutShiftTracker().Score();
EXPECT_GT(score, 0);
// Now scroll the element back off-screen.
GetDocument().domWindow()->scrollTo(0, 0);
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(score, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 100), offscreen->Size());
EXPECT_EQ(LayoutSize(100, 1), onscreen->Size());
// In the subsequent frame, #offscreen becomes locked and changes its
// layout size (and vice-versa for #onscreen).
UpdateAllLifecyclePhasesForTest();
offscreen = To<LayoutBox>(GetLayoutObjectByElementId("offscreen"));
onscreen = To<LayoutBox>(GetLayoutObjectByElementId("onscreen"));
EXPECT_FLOAT_EQ(score, GetLayoutShiftTracker().Score());
EXPECT_EQ(LayoutSize(100, 1), offscreen->Size());
EXPECT_EQ(LayoutSize(100, 100), onscreen->Size());
}
TEST_F(LayoutShiftTrackerTest, NestedFixedPos) {
SetBodyInnerHTML(R"HTML(
<div id=parent style="position: fixed; top: 0; left: -100%; width: 100%">
<div id=target style="position: fixed; top: 0; width: 100%; height: 100%;
left: 0"; background: blue></div>
</div>
<div style="height: 5000px"></div>
</div>
)HTML");
auto* target = To<LayoutBox>(GetLayoutObjectByElementId("target"));
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
// Test that repaint of #target does not record a layout shift.
target->SetNeedsPaintPropertyUpdate();
target->SetSubtreeShouldDoFullPaintInvalidation();
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
}
TEST_F(LayoutShiftTrackerTest, ClipByVisualViewport) {
SetHtmlInnerHTML(R"HTML(
<meta name="viewport" content="width=200, initial-scale=2">
<style>
#target {
position: absolute;
top: 0;
left: 150px;
width: 200px;
height: 200px;
background: blue;
}
</style>
<div id=target></div>
)HTML");
GetDocument().GetPage()->GetVisualViewport().SetSize(IntSize(200, 500));
GetDocument().GetPage()->GetVisualViewport().SetLocation(FloatPoint(0, 100));
UpdateAllLifecyclePhasesForTest();
// The visual viewport.
EXPECT_EQ(IntRect(0, 100, 200, 500),
GetDocument().View()->GetScrollableArea()->VisibleContentRect());
// The layout viewport .
EXPECT_EQ(IntRect(0, 0, 800, 600),
GetDocument().View()->LayoutViewport()->VisibleContentRect());
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
GetDocument().getElementById("target")->setAttribute(html_names::kStyleAttr,
"top: 100px");
UpdateAllLifecyclePhasesForTest();
// 50.0: visible width
// 100.0 + 100.0: visible height + vertical shift
// 200.0 * 500.0: visual viewport area
// 100.0 / 500.0: shift distance fraction
EXPECT_FLOAT_EQ(50.0 * (100.0 + 100.0) / (200.0 * 500.0) * (100.0 / 500.0),
GetLayoutShiftTracker().Score());
}
TEST_F(LayoutShiftTrackerTest, ScrollThenCauseScrollAnchoring) {
SetBodyInnerHTML(R"HTML(
<style>
.big {
width: 100px;
height: 500px;
background: blue;
}
.small {
width: 100px;
height: 100px;
background: green;
}
</style>
<div class=big id=target></div>
<div class=big></div>
<div class=big></div>
<div class=big></div>
<div class=big></div>
<div class=big></div>
)HTML");
auto* target_element = GetDocument().getElementById("target");
// Scroll the window which accumulates a scroll in the layout shift tracker.
GetDocument().domWindow()->scrollBy(0, 1000);
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
target_element->classList().Remove("big");
target_element->classList().Add("small");
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
target_element->classList().Remove("small");
target_element->classList().Add("big");
UpdateAllLifecyclePhasesForTest();
EXPECT_FLOAT_EQ(0, GetLayoutShiftTracker().Score());
}
TEST_F(LayoutShiftTrackerTest, NeedsToTrack) {
SetBodyInnerHTML(R"HTML(
<style>* { width: 50px; height: 50px; }</style>
<div id="tiny" style="width: 0.3px; height: 0.3px; background: blue"></div>
<div id="sticky" style="background: blue; position: sticky"></div>
<!-- block with decoration -->
<div id="scroll" style="overflow: scroll"></div>
<div id="background" style="background: blue"></div>
<div id="border" style="border: 1px solid black"></div>
<div id="outline" style="outline: 1px solid black"></div>
<div id="shadow" style="box-shadow: 2px 2px black"></div>
<!-- block with block children, some invisible -->
<div id="hidden-parent">
<div id="hidden" style="background: blue; visibility: hidden">
<div id="visible-under-hidden"
style="background:blue; visibility: visible"></div>
</div>
</div>
<!-- block with inline children, some invisible -->
<div id="empty-parent">
<div id="empty"></div>
</div>
<div id="text-block">Text</div>
<br id="br">
<svg id="svg">
<rect id="svg-rect" width="10" height="10" fill="green">
</svg>
<!-- replaced, special blocks, etc. -->
<video id="video"></video>
<img id="img">
<textarea id="textarea">Text</textarea>
<input id="text-input" type="text">
<input id="file" type="file">
<input id="radio" type="radio">
<progress id="progress"></progress>
<ul>
<li id="li"></li>
</ul>
<hr id="hr">
)HTML");
const auto& tracker = GetLayoutShiftTracker();
EXPECT_FALSE(tracker.NeedsToTrack(GetLayoutView()));
EXPECT_FALSE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("tiny")));
EXPECT_FALSE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("sticky")));
// Blocks with decorations.
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("scroll")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("background")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("border")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("outline")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("shadow")));
// Blocks with block children, some invisible. We don't check descendants for
// visibility. Just assume there are visible descendants.
EXPECT_TRUE(
tracker.NeedsToTrack(*GetLayoutObjectByElementId("empty-parent")));
EXPECT_FALSE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("empty")));
EXPECT_TRUE(
tracker.NeedsToTrack(*GetLayoutObjectByElementId("hidden-parent")));
EXPECT_FALSE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("hidden")));
EXPECT_TRUE(tracker.NeedsToTrack(
*GetLayoutObjectByElementId("visible-under-hidden")));
// Blocks with inline children, some invisible. We don't check descendants for
// visibility. Just assume there are visible descendants.
auto* text_block = To<LayoutBlock>(GetLayoutObjectByElementId("text-block"));
EXPECT_TRUE(tracker.NeedsToTrack(*text_block));
// No ContainingBlockScope.
EXPECT_FALSE(tracker.NeedsToTrack(*text_block->FirstChild()));
{
LayoutShiftTracker::ContainingBlockScope scope(
PhysicalSize(1, 2), PhysicalSize(2, 3), PhysicalRect(1, 2, 3, 4),
PhysicalRect(2, 3, 4, 5));
EXPECT_TRUE(tracker.NeedsToTrack(*text_block->FirstChild()));
}
auto* br = GetLayoutObjectByElementId("br");
EXPECT_FALSE(tracker.NeedsToTrack(*br));
EXPECT_TRUE(br->Parent()->IsAnonymous());
EXPECT_FALSE(tracker.NeedsToTrack(*br->Parent()));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("svg")));
// We don't track SVG children.
EXPECT_FALSE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("svg-rect")));
// Replaced, special blocks, etc.
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("video")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("img")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("textarea")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("text-input")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("file")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("radio")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("progress")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("li")));
EXPECT_TRUE(tracker.NeedsToTrack(*GetLayoutObjectByElementId("hr")));
}
} // namespace blink