blob: fdc6c75f36f81ae6b7c1dc52dac926dc7e0011ed [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/css/resolver/style_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/animation/animation_test_helpers.h"
#include "third_party/blink/renderer/core/animation/document_timeline.h"
#include "third_party/blink/renderer/core/animation/element_animations.h"
#include "third_party/blink/renderer/core/css/css_image_value.h"
#include "third_party/blink/renderer/core/css/css_test_helpers.h"
#include "third_party/blink/renderer/core/css/css_value_list.h"
#include "third_party/blink/renderer/core/css/properties/computed_style_utils.h"
#include "third_party/blink/renderer/core/css/properties/css_property_ref.h"
#include "third_party/blink/renderer/core/css/style_change_reason.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/css/style_sheet_contents.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/pseudo_element.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
namespace blink {
using animation_test_helpers::CreateSimpleKeyframeEffectForTest;
class StyleResolverTest : public PageTestBase {
public:
scoped_refptr<ComputedStyle> StyleForId(AtomicString id) {
Element* element = GetDocument().getElementById(id);
auto style = GetStyleEngine().GetStyleResolver().StyleForElement(
element, StyleRecalcContext());
DCHECK(style);
return style;
}
String ComputedValue(String name, const ComputedStyle& style) {
CSSPropertyRef ref(name, GetDocument());
DCHECK(ref.IsValid());
return ref.GetProperty()
.CSSValueFromComputedStyle(style, nullptr, false)
->CssText();
}
protected:
};
TEST_F(StyleResolverTest, StyleForTextInDisplayNone) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<body style="display:none">Text</body>
)HTML");
UpdateAllLifecyclePhasesForTest();
GetDocument().body()->EnsureComputedStyle();
ASSERT_TRUE(GetDocument().body()->GetComputedStyle());
EXPECT_TRUE(
GetDocument().body()->GetComputedStyle()->IsEnsuredInDisplayNone());
EXPECT_FALSE(GetStyleEngine().GetStyleResolver().StyleForText(
To<Text>(GetDocument().body()->firstChild())));
}
TEST_F(StyleResolverTest, AnimationBaseComputedStyle) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
html { font-size: 10px; }
body { font-size: 20px; }
@keyframes fade { to { opacity: 0; }}
#div { animation: fade 1s; }
</style>
<div id="div">Test</div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
ElementAnimations& animations = div->EnsureElementAnimations();
animations.SetAnimationStyleChange(true);
StyleResolver& resolver = GetStyleEngine().GetStyleResolver();
ASSERT_TRUE(resolver.StyleForElement(div, StyleRecalcContext()));
EXPECT_EQ(20,
resolver.StyleForElement(div, StyleRecalcContext())->FontSize());
ASSERT_TRUE(animations.BaseComputedStyle());
EXPECT_EQ(20, animations.BaseComputedStyle()->FontSize());
// Getting style with customized parent style should not affect cached
// animation base computed style.
const ComputedStyle* parent_style =
GetDocument().documentElement()->GetComputedStyle();
EXPECT_EQ(10, resolver
.StyleForElement(div, StyleRecalcContext(), parent_style,
parent_style)
->FontSize());
ASSERT_TRUE(animations.BaseComputedStyle());
EXPECT_EQ(20, animations.BaseComputedStyle()->FontSize());
EXPECT_EQ(20,
resolver.StyleForElement(div, StyleRecalcContext())->FontSize());
}
TEST_F(StyleResolverTest, HasEmUnits) {
GetDocument().documentElement()->setInnerHTML("<div id=div>Test</div>");
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(StyleForId("div")->HasEmUnits());
GetDocument().documentElement()->setInnerHTML(
"<div id=div style='width:1em'>Test</div>");
UpdateAllLifecyclePhasesForTest();
EXPECT_TRUE(StyleForId("div")->HasEmUnits());
}
TEST_F(StyleResolverTest, BaseReusableIfFontRelativeUnitsAbsent) {
GetDocument().documentElement()->setInnerHTML("<div id=div>Test</div>");
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(
div, CSSPropertyID::kFontSize, "50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("50px", ComputedValue("font-size", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
StyleForId("div");
ASSERT_TRUE(div->GetElementAnimations());
EXPECT_TRUE(div->GetElementAnimations()->BaseComputedStyle());
StyleResolverState state(GetDocument(), *div);
EXPECT_TRUE(StyleResolver::CanReuseBaseComputedStyle(state));
}
TEST_F(StyleResolverTest, AnimationNotMaskedByImportant) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
div {
width: 10px;
height: 10px !important;
}
</style>
<div id=div></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(div, CSSPropertyID::kWidth,
"50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("50px", ComputedValue("width", *StyleForId("div")));
EXPECT_EQ("10px", ComputedValue("height", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
StyleForId("div");
ASSERT_TRUE(div->GetElementAnimations());
const CSSBitset* bitset = div->GetElementAnimations()->BaseImportantSet();
EXPECT_FALSE(CSSAnimations::IsAnimatingStandardProperties(
div->GetElementAnimations(), bitset, KeyframeEffect::kDefaultPriority));
EXPECT_TRUE(div->GetElementAnimations()->BaseComputedStyle());
EXPECT_FALSE(bitset && bitset->Has(CSSPropertyID::kWidth));
EXPECT_TRUE(bitset && bitset->Has(CSSPropertyID::kHeight));
}
TEST_F(StyleResolverTest, AnimationNotMaskedWithoutElementAnimations) {
EXPECT_FALSE(CSSAnimations::IsAnimatingStandardProperties(
/* ElementAnimations */ nullptr, std::make_unique<CSSBitset>().get(),
KeyframeEffect::kDefaultPriority));
}
TEST_F(StyleResolverTest, AnimationNotMaskedWithoutBitset) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
div {
width: 10px;
height: 10px !important;
}
</style>
<div id=div></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(div, CSSPropertyID::kWidth,
"50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("50px", ComputedValue("width", *StyleForId("div")));
EXPECT_EQ("10px", ComputedValue("height", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
StyleForId("div");
ASSERT_TRUE(div->GetElementAnimations());
EXPECT_FALSE(CSSAnimations::IsAnimatingStandardProperties(
div->GetElementAnimations(), /* CSSBitset */ nullptr,
KeyframeEffect::kDefaultPriority));
}
TEST_F(StyleResolverTest, AnimationMaskedByImportant) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
div {
width: 10px;
height: 10px !important;
}
</style>
<div id=div></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(div, CSSPropertyID::kHeight,
"50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("10px", ComputedValue("width", *StyleForId("div")));
EXPECT_EQ("10px", ComputedValue("height", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
StyleForId("div");
ASSERT_TRUE(div->GetElementAnimations());
EXPECT_TRUE(div->GetElementAnimations()->BaseComputedStyle());
EXPECT_TRUE(div->GetElementAnimations()->BaseImportantSet());
StyleResolverState state(GetDocument(), *div);
EXPECT_FALSE(StyleResolver::CanReuseBaseComputedStyle(state));
}
TEST_F(StyleResolverTest,
TransitionRetargetRelativeFontSizeOnParentlessElement) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
html {
font-size: 20px;
transition: font-size 100ms;
}
.adjust { font-size: 50%; }
</style>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* element = GetDocument().documentElement();
element->setAttribute(html_names::kIdAttr, "target");
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("20px", ComputedValue("font-size", *StyleForId("target")));
ElementAnimations* element_animations = element->GetElementAnimations();
EXPECT_FALSE(element_animations);
// Trigger a transition with a dependency on the parent style.
element->setAttribute(html_names::kClassAttr, "adjust");
UpdateAllLifecyclePhasesForTest();
element_animations = element->GetElementAnimations();
EXPECT_TRUE(element_animations);
Animation* transition = (*element_animations->Animations().begin()).key;
EXPECT_TRUE(transition);
EXPECT_EQ("20px", ComputedValue("font-size", *StyleForId("target")));
// Bump the animation time to ensure a transition reversal.
transition->setCurrentTime(CSSNumberish::FromDouble(50));
transition->pause();
UpdateAllLifecyclePhasesForTest();
const String before_reversal_font_size =
ComputedValue("font-size", *StyleForId("target"));
// Verify there is no discontinuity in the font-size on transition reversal.
element->setAttribute(html_names::kClassAttr, "");
UpdateAllLifecyclePhasesForTest();
element_animations = element->GetElementAnimations();
EXPECT_TRUE(element_animations);
Animation* reverse_transition =
(*element_animations->Animations().begin()).key;
EXPECT_TRUE(reverse_transition);
EXPECT_EQ(before_reversal_font_size,
ComputedValue("font-size", *StyleForId("target")));
}
TEST_F(StyleResolverTest, NonCachableStyleCheckDoesNotAffectBaseComputedStyle) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
.adjust { color: rgb(0, 0, 0); }
</style>
<div>
<div style="color: rgb(0, 128, 0)">
<div id="target" style="transition: color 1s linear"></div>
</div>
</div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* target = GetDocument().getElementById("target");
EXPECT_EQ("rgb(0, 128, 0)", ComputedValue("color", *StyleForId("target")));
// Trigger a transition on an inherited property.
target->setAttribute(html_names::kClassAttr, "adjust");
UpdateAllLifecyclePhasesForTest();
ElementAnimations* element_animations = target->GetElementAnimations();
EXPECT_TRUE(element_animations);
Animation* transition = (*element_animations->Animations().begin()).key;
EXPECT_TRUE(transition);
// Advance to the midpoint of the transition.
transition->setCurrentTime(CSSNumberish::FromDouble(500));
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("rgb(0, 64, 0)", ComputedValue("color", *StyleForId("target")));
EXPECT_TRUE(element_animations->BaseComputedStyle());
element_animations->ClearBaseComputedStyle();
// Perform a non-cacheable style resolution, and ensure that the base computed
// style is not updated.
GetStyleEngine().GetStyleResolver().StyleForElement(
target, StyleRecalcContext(), nullptr, nullptr,
kMatchAllRulesExcludingSMIL);
EXPECT_FALSE(element_animations->BaseComputedStyle());
// Computing the style with default args updates the base computed style.
EXPECT_EQ("rgb(0, 64, 0)", ComputedValue("color", *StyleForId("target")));
EXPECT_TRUE(element_animations->BaseComputedStyle());
}
class StyleResolverFontRelativeUnitTest
: public testing::WithParamInterface<const char*>,
public StyleResolverTest {};
TEST_P(StyleResolverFontRelativeUnitTest,
BaseNotReusableIfFontRelativeUnitPresent) {
GetDocument().documentElement()->setInnerHTML(
String::Format("<div id=div style='width:1%s'>Test</div>", GetParam()));
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(
div, CSSPropertyID::kFontSize, "50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("50px", ComputedValue("font-size", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
auto computed_style = StyleForId("div");
EXPECT_TRUE(computed_style->HasFontRelativeUnits());
ASSERT_TRUE(div->GetElementAnimations());
EXPECT_TRUE(div->GetElementAnimations()->BaseComputedStyle());
StyleResolverState state(GetDocument(), *div);
EXPECT_FALSE(StyleResolver::CanReuseBaseComputedStyle(state));
}
TEST_P(StyleResolverFontRelativeUnitTest,
BaseReusableIfNoFontAffectingAnimation) {
GetDocument().documentElement()->setInnerHTML(
String::Format("<div id=div style='width:1%s'>Test</div>", GetParam()));
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(div, CSSPropertyID::kHeight,
"50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("50px", ComputedValue("height", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
auto computed_style = StyleForId("div");
EXPECT_TRUE(computed_style->HasFontRelativeUnits());
ASSERT_TRUE(div->GetElementAnimations());
EXPECT_TRUE(div->GetElementAnimations()->BaseComputedStyle());
StyleResolverState state(GetDocument(), *div);
EXPECT_TRUE(StyleResolver::CanReuseBaseComputedStyle(state));
}
INSTANTIATE_TEST_SUITE_P(All,
StyleResolverFontRelativeUnitTest,
testing::Values("em", "rem", "ex", "ch"));
// TODO(crbug.com/1180159): Remove this test when @container and transitions
// work properly.
TEST_F(StyleResolverTest, BaseNotReusableWithContainerQueries) {
ScopedCSSContainerQueriesForTest scoped_feature(true);
GetDocument().documentElement()->setInnerHTML("<div id=div>Test</div>");
UpdateAllLifecyclePhasesForTest();
Element* div = GetDocument().getElementById("div");
auto* effect = CreateSimpleKeyframeEffectForTest(div, CSSPropertyID::kWidth,
"50px", "100px");
GetDocument().Timeline().Play(effect);
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("50px", ComputedValue("width", *StyleForId("div")));
div->SetNeedsAnimationStyleRecalc();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
StyleForId("div");
StyleResolverState state(GetDocument(), *div);
EXPECT_FALSE(StyleResolver::CanReuseBaseComputedStyle(state));
}
namespace {
const CSSImageValue& GetBackgroundImageValue(const ComputedStyle& style) {
const CSSValue* computed_value = ComputedStyleUtils::ComputedPropertyValue(
GetCSSPropertyBackgroundImage(), style);
const CSSValueList* bg_img_list = To<CSSValueList>(computed_value);
return To<CSSImageValue>(bg_img_list->Item(0));
}
const CSSImageValue& GetBackgroundImageValue(const Element* element) {
DCHECK(element);
return GetBackgroundImageValue(element->ComputedStyleRef());
}
} // namespace
TEST_F(StyleResolverTest, BackgroundImageFetch) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
#none {
display: none;
background-image: url(img-none.png);
}
#inside-none {
background-image: url(img-inside-none.png);
}
#hidden {
visibility: hidden;
background-image: url(img-hidden.png);
}
#inside-hidden {
background-image: url(img-inside-hidden.png);
}
#contents {
display: contents;
background-image: url(img-contents.png);
}
#inside-contents-parent {
display: contents;
background-image: url(img-inside-contents.png);
}
#inside-contents {
background-image: inherit;
}
#non-slotted {
background-image: url(img-non-slotted.png);
}
#no-pseudo::before {
background-image: url(img-no-pseudo.png);
}
#first-line::first-line {
background-image: url(first-line.png);
}
#first-line-span::first-line {
background-image: url(first-line-span.png);
}
#first-line-none { display: none; }
#first-line-none::first-line {
background-image: url(first-line-none.png);
}
frameset {
display: none;
border-color: currentColor; /* UA inherit defeats caching */
background-image: url(frameset-none.png);
}
</style>
<div id="none">
<div id="inside-none"></div>
</div>
<div id="hidden">
<div id="inside-hidden"></div>
</div>
<div id="contents"></div>
<div id="inside-contents-parent">
<div id="inside-contents"></div>
</div>
<div id="host">
<div id="non-slotted"></div>
</div>
<div id="no-pseudo"></div>
<div id="first-line">XXX</div>
<span id="first-line-span">XXX</span>
<div id="first-line-none">XXX</div>
)HTML");
auto* frameset1 = GetDocument().CreateRawElement(html_names::kFramesetTag);
auto* frameset2 = GetDocument().CreateRawElement(html_names::kFramesetTag);
GetDocument().documentElement()->AppendChild(frameset1);
GetDocument().documentElement()->AppendChild(frameset2);
GetDocument().getElementById("host")->AttachShadowRootInternal(
ShadowRootType::kOpen);
UpdateAllLifecyclePhasesForTest();
auto* none = GetDocument().getElementById("none");
auto* inside_none = GetDocument().getElementById("inside-none");
auto* hidden = GetDocument().getElementById("hidden");
auto* inside_hidden = GetDocument().getElementById("inside-hidden");
auto* contents = GetDocument().getElementById("contents");
auto* inside_contents = GetDocument().getElementById("inside-contents");
auto* non_slotted = GetDocument().getElementById("non-slotted");
auto* no_pseudo = GetDocument().getElementById("no-pseudo");
auto* first_line = GetDocument().getElementById("first-line");
auto* first_line_span = GetDocument().getElementById("first-line-span");
auto* first_line_none = GetDocument().getElementById("first-line-none");
inside_none->EnsureComputedStyle();
non_slotted->EnsureComputedStyle();
auto* before_style = no_pseudo->EnsureComputedStyle(kPseudoIdBefore);
auto* first_line_style = first_line->EnsureComputedStyle(kPseudoIdFirstLine);
auto* first_line_span_style =
first_line_span->EnsureComputedStyle(kPseudoIdFirstLine);
auto* first_line_none_style =
first_line_none->EnsureComputedStyle(kPseudoIdFirstLine);
ASSERT_TRUE(before_style);
EXPECT_TRUE(GetBackgroundImageValue(*before_style).IsCachePending())
<< "No fetch for non-generated ::before";
ASSERT_TRUE(first_line_style);
EXPECT_FALSE(GetBackgroundImageValue(*first_line_style).IsCachePending())
<< "Fetched by layout of ::first-line";
ASSERT_TRUE(first_line_span_style);
EXPECT_TRUE(GetBackgroundImageValue(*first_line_span_style).IsCachePending())
<< "No fetch for inline with ::first-line";
ASSERT_TRUE(first_line_none_style);
EXPECT_TRUE(GetBackgroundImageValue(*first_line_none_style).IsCachePending())
<< "No fetch for display:none with ::first-line";
EXPECT_TRUE(GetBackgroundImageValue(none).IsCachePending())
<< "No fetch for display:none";
EXPECT_TRUE(GetBackgroundImageValue(inside_none).IsCachePending())
<< "No fetch inside display:none";
EXPECT_FALSE(GetBackgroundImageValue(hidden).IsCachePending())
<< "Fetch for visibility:hidden";
EXPECT_FALSE(GetBackgroundImageValue(inside_hidden).IsCachePending())
<< "Fetch for inherited visibility:hidden";
EXPECT_FALSE(GetBackgroundImageValue(contents).IsCachePending())
<< "Fetch for display:contents";
EXPECT_FALSE(GetBackgroundImageValue(inside_contents).IsCachePending())
<< "Fetch for image inherited from display:contents";
EXPECT_TRUE(GetBackgroundImageValue(non_slotted).IsCachePending())
<< "No fetch for element outside the flat tree";
// Added two frameset elements to hit the MatchedPropertiesCache for the
// second one. Frameset adjusts style to display:block in StyleAdjuster, but
// adjustments are not run before ComputedStyle is added to the
// MatchedPropertiesCache leaving the cached style with StylePendingImage
// unless we also check for LayoutObjectIsNeeded in
// StyleResolverState::LoadPendingImages.
EXPECT_FALSE(GetBackgroundImageValue(frameset1).IsCachePending())
<< "Fetch for display:none frameset";
EXPECT_FALSE(GetBackgroundImageValue(frameset2).IsCachePending())
<< "Fetch for display:none frameset - cached";
}
TEST_F(StyleResolverTest, NoFetchForAtPage) {
// Strictly, we should drop descriptors from @page rules which are not valid
// descriptors, but as long as we apply them to ComputedStyle we should at
// least not trigger fetches. The display:contents is here to make sure we
// don't hit a DCHECK in StylePendingImage::ComputedCSSValue().
GetDocument().body()->setInnerHTML(R"HTML(
<style>
@page {
display: contents;
background-image: url(bg-img.png);
}
</style>
)HTML");
GetDocument().GetStyleEngine().UpdateActiveStyle();
scoped_refptr<const ComputedStyle> page_style =
GetDocument().GetStyleResolver().StyleForPage(0, "");
ASSERT_TRUE(page_style);
const CSSValue* computed_value = ComputedStyleUtils::ComputedPropertyValue(
GetCSSPropertyBackgroundImage(), *page_style);
const CSSValueList* bg_img_list = To<CSSValueList>(computed_value);
EXPECT_TRUE(To<CSSImageValue>(bg_img_list->Item(0)).IsCachePending());
}
TEST_F(StyleResolverTest, NoFetchForHighlightPseudoElements) {
ScopedCSSTargetTextPseudoElementForTest scoped_feature(true);
GetDocument().body()->setInnerHTML(R"HTML(
<style>
body::target-text, body::selection {
color: green;
background-image: url(bg-img.png);
cursor: url(cursor.ico), auto;
}
</style>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto* body = GetDocument().body();
ASSERT_TRUE(body);
const auto* element_style = body->GetComputedStyle();
ASSERT_TRUE(element_style);
scoped_refptr<ComputedStyle> target_text_style =
GetDocument().GetStyleResolver().PseudoStyleForElement(
GetDocument().body(), StyleRecalcContext(),
PseudoElementStyleRequest(kPseudoIdTargetText), element_style,
element_style);
ASSERT_TRUE(target_text_style);
scoped_refptr<ComputedStyle> selection_style =
GetDocument().GetStyleResolver().PseudoStyleForElement(
GetDocument().body(), StyleRecalcContext(),
PseudoElementStyleRequest(kPseudoIdSelection), element_style,
element_style);
ASSERT_TRUE(selection_style);
// Check that we don't fetch the cursor url() for ::target-text.
CursorList* cursor_list = target_text_style->Cursors();
ASSERT_TRUE(cursor_list->size());
CursorData& current_cursor = cursor_list->at(0);
StyleImage* image = current_cursor.GetImage();
ASSERT_TRUE(image);
EXPECT_TRUE(image->IsPendingImage());
for (const auto* pseudo_style :
{target_text_style.get(), selection_style.get()}) {
// Check that the color applies.
EXPECT_EQ(Color(0, 128, 0),
pseudo_style->VisitedDependentColor(GetCSSPropertyColor()));
// Check that the background-image does not apply.
const CSSValue* computed_value = ComputedStyleUtils::ComputedPropertyValue(
GetCSSPropertyBackgroundImage(), *pseudo_style);
const CSSValueList* list = DynamicTo<CSSValueList>(computed_value);
ASSERT_TRUE(list);
ASSERT_EQ(1u, list->length());
const auto* keyword = DynamicTo<CSSIdentifierValue>(list->Item(0));
ASSERT_TRUE(keyword);
EXPECT_EQ(CSSValueID::kNone, keyword->GetValueID());
}
}
TEST_F(StyleResolverTest, CSSMarkerPseudoElement) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
b::before {
content: "[before]";
display: list-item;
}
#marker ::marker {
color: blue;
}
</style>
<ul>
<li style="list-style: decimal outside"><b></b></li>
<li style="list-style: decimal inside"><b></b></li>
<li style="list-style: disc outside"><b></b></li>
<li style="list-style: disc inside"><b></b></li>
<li style="list-style: '- ' outside"><b></b></li>
<li style="list-style: '- ' inside"><b></b></li>
<li style="list-style: linear-gradient(blue, cyan) outside"><b></b></li>
<li style="list-style: linear-gradient(blue, cyan) inside"><b></b></li>
<li style="list-style: none outside"><b></b></li>
<li style="list-style: none inside"><b></b></li>
</ul>
)HTML");
StaticElementList* lis = GetDocument().QuerySelectorAll("li");
EXPECT_EQ(lis->length(), 10U);
UpdateAllLifecyclePhasesForTest();
for (unsigned i = 0; i < lis->length(); ++i) {
Element* li = lis->item(i);
PseudoElement* marker = li->GetPseudoElement(kPseudoIdMarker);
PseudoElement* before =
li->QuerySelector("b")->GetPseudoElement(kPseudoIdBefore);
PseudoElement* nested_marker = before->GetPseudoElement(kPseudoIdMarker);
// Check that UA styles for list markers don't set HasPseudoElementStyle
const ComputedStyle* li_style = li->GetComputedStyle();
EXPECT_FALSE(li_style->HasPseudoElementStyle(kPseudoIdMarker));
EXPECT_FALSE(li_style->HasAnyPseudoElementStyles());
const ComputedStyle* before_style = before->GetComputedStyle();
EXPECT_FALSE(before_style->HasPseudoElementStyle(kPseudoIdMarker));
EXPECT_FALSE(before_style->HasAnyPseudoElementStyles());
if (i >= 8) {
EXPECT_FALSE(marker);
EXPECT_FALSE(nested_marker);
continue;
}
// Check that list markers have UA styles
EXPECT_TRUE(marker);
EXPECT_TRUE(nested_marker);
EXPECT_EQ(marker->GetComputedStyle()->GetUnicodeBidi(),
UnicodeBidi::kIsolate);
EXPECT_EQ(nested_marker->GetComputedStyle()->GetUnicodeBidi(),
UnicodeBidi::kIsolate);
}
GetDocument().body()->SetIdAttribute("marker");
UpdateAllLifecyclePhasesForTest();
for (unsigned i = 0; i < lis->length(); ++i) {
Element* li = lis->item(i);
PseudoElement* before =
li->QuerySelector("b")->GetPseudoElement(kPseudoIdBefore);
// Check that author styles for list markers do set HasPseudoElementStyle
const ComputedStyle* li_style = li->GetComputedStyle();
EXPECT_TRUE(li_style->HasPseudoElementStyle(kPseudoIdMarker));
EXPECT_TRUE(li_style->HasAnyPseudoElementStyles());
// But ::marker styles don't match a ::before::marker
const ComputedStyle* before_style = before->GetComputedStyle();
EXPECT_FALSE(before_style->HasPseudoElementStyle(kPseudoIdMarker));
EXPECT_FALSE(before_style->HasAnyPseudoElementStyles());
}
}
TEST_F(StyleResolverTest, ApplyInheritedOnlyCustomPropertyChange) {
// This test verifies that when we get a "apply inherited only"-type
// hit in the MatchesPropertiesCache, we're able to detect that custom
// properties changed, and that we therefore need to apply the non-inherited
// properties as well.
GetDocument().body()->setInnerHTML(R"HTML(
<style>
#parent1 { --a: 10px; }
#parent2 { --a: 20px; }
#child1, #child2 {
--b: var(--a);
width: var(--b);
}
</style>
<div id=parent1><div id=child1></div></div>
<div id=parent2><div id=child2></div></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
EXPECT_EQ("10px", ComputedValue("width", *StyleForId("child1")));
EXPECT_EQ("20px", ComputedValue("width", *StyleForId("child2")));
}
TEST_F(StyleResolverTest, CssRulesForElementIncludedRules) {
UpdateAllLifecyclePhasesForTest();
Element* body = GetDocument().body();
ASSERT_TRUE(body);
// Don't crash when only getting one type of rule.
auto& resolver = GetDocument().GetStyleResolver();
resolver.CssRulesForElement(body, StyleResolver::kUACSSRules);
resolver.CssRulesForElement(body, StyleResolver::kUserCSSRules);
resolver.CssRulesForElement(body, StyleResolver::kAuthorCSSRules);
}
TEST_F(StyleResolverTest, NestedPseudoElement) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
div::before { content: "Hello"; display: list-item; }
div::before::marker { color: green; }
</style>
)HTML");
UpdateAllLifecyclePhasesForTest();
// Don't crash when calculating style for nested pseudo elements.
}
TEST_F(StyleResolverTest, CascadedValuesForElement) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
#div {
top: 1em;
}
div {
top: 10em;
right: 20em;
bottom: 30em;
left: 40em;
width: 50em;
width: 51em;
height: 60em !important;
height: 61em;
}
</style>
<div id=div style="bottom:300em;"></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& resolver = GetDocument().GetStyleResolver();
Element* div = GetDocument().getElementById("div");
ASSERT_TRUE(div);
auto map = resolver.CascadedValuesForElement(div, kPseudoIdNone);
CSSPropertyName top(CSSPropertyID::kTop);
CSSPropertyName right(CSSPropertyID::kRight);
CSSPropertyName bottom(CSSPropertyID::kBottom);
CSSPropertyName left(CSSPropertyID::kLeft);
CSSPropertyName width(CSSPropertyID::kWidth);
CSSPropertyName height(CSSPropertyID::kHeight);
ASSERT_TRUE(map.at(top));
ASSERT_TRUE(map.at(right));
ASSERT_TRUE(map.at(bottom));
ASSERT_TRUE(map.at(left));
ASSERT_TRUE(map.at(width));
ASSERT_TRUE(map.at(height));
EXPECT_EQ("1em", map.at(top)->CssText());
EXPECT_EQ("20em", map.at(right)->CssText());
EXPECT_EQ("300em", map.at(bottom)->CssText());
EXPECT_EQ("40em", map.at(left)->CssText());
EXPECT_EQ("51em", map.at(width)->CssText());
EXPECT_EQ("60em", map.at(height)->CssText());
}
TEST_F(StyleResolverTest, CascadedValuesForPseudoElement) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
#div::before {
top: 1em;
}
div::before {
top: 10em;
}
</style>
<div id=div></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto& resolver = GetDocument().GetStyleResolver();
Element* div = GetDocument().getElementById("div");
ASSERT_TRUE(div);
auto map = resolver.CascadedValuesForElement(div, kPseudoIdBefore);
CSSPropertyName top(CSSPropertyID::kTop);
ASSERT_TRUE(map.at(top));
EXPECT_EQ("1em", map.at(top)->CssText());
}
TEST_F(StyleResolverTest, EnsureComputedStyleSlotFallback) {
GetDocument().body()->setInnerHTML(R"HTML(
<div id="host"><span></span></div>
)HTML");
ShadowRoot& shadow_root =
GetDocument().getElementById("host")->AttachShadowRootInternal(
ShadowRootType::kOpen);
shadow_root.setInnerHTML(R"HTML(
<style>
slot { color: red }
</style>
<slot><span id="fallback"></span></slot>
)HTML");
Element* fallback = shadow_root.getElementById("fallback");
ASSERT_TRUE(fallback);
UpdateAllLifecyclePhasesForTest();
// Elements outside the flat tree does not get styles computed during the
// lifecycle update.
EXPECT_FALSE(fallback->GetComputedStyle());
// We are currently allowed to query the computed style of elements outside
// the flat tree, but slot fallback does not inherit from the slot.
const ComputedStyle* fallback_style = fallback->EnsureComputedStyle();
ASSERT_TRUE(fallback_style);
EXPECT_EQ(Color::kBlack,
fallback_style->VisitedDependentColor(GetCSSPropertyColor()));
}
TEST_F(StyleResolverTest, ComputeValueStandardProperty) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
#target { --color: green }
</style>
<div id="target"></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* target = GetDocument().getElementById("target");
ASSERT_TRUE(target);
// Unable to parse a variable reference with css_test_helpers::ParseLonghand.
CSSPropertyID property_id = CSSPropertyID::kColor;
auto* set =
MakeGarbageCollected<MutableCSSPropertyValueSet>(kHTMLStandardMode);
MutableCSSPropertyValueSet::SetResult result = set->SetProperty(
property_id, "var(--color)", false, SecureContextMode::kInsecureContext,
/*style_sheet_contents=*/nullptr);
ASSERT_TRUE(result.did_parse);
const CSSValue* parsed_value = set->GetPropertyCSSValue(property_id);
ASSERT_TRUE(parsed_value);
const CSSValue* computed_value = StyleResolver::ComputeValue(
target, CSSPropertyName(property_id), *parsed_value);
ASSERT_TRUE(computed_value);
EXPECT_EQ("rgb(0, 128, 0)", computed_value->CssText());
}
TEST_F(StyleResolverTest, ComputeValueCustomProperty) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
#target { --color: green }
</style>
<div id="target"></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
Element* target = GetDocument().getElementById("target");
ASSERT_TRUE(target);
AtomicString custom_property_name = "--color";
const CSSValue* parsed_value = css_test_helpers::ParseLonghand(
GetDocument(), CustomProperty(custom_property_name, GetDocument()),
"blue");
ASSERT_TRUE(parsed_value);
const CSSValue* computed_value = StyleResolver::ComputeValue(
target, CSSPropertyName(custom_property_name), *parsed_value);
ASSERT_TRUE(computed_value);
EXPECT_EQ("blue", computed_value->CssText());
}
TEST_F(StyleResolverTest, TreeScopedReferences) {
GetDocument().body()->setInnerHTML(R"HTML(
<style>
#host { animation-name: anim }
</style>
<div id="host">
<span id="slotted"></span>
</host>
)HTML");
Element* host = GetDocument().getElementById("host");
ASSERT_TRUE(host);
ShadowRoot& root = host->AttachShadowRootInternal(ShadowRootType::kOpen);
root.setInnerHTML(R"HTML(
<style>
::slotted(span) { animation-name: anim-slotted }
:host { font-family: myfont }
</style>
<div id="inner-host">
<slot></slot>
</div>
)HTML");
Element* inner_host = root.getElementById("inner-host");
ASSERT_TRUE(inner_host);
ShadowRoot& inner_root =
inner_host->AttachShadowRootInternal(ShadowRootType::kOpen);
inner_root.setInnerHTML(R"HTML(
<style>
::slotted(span) { animation-name: anim-inner-slotted }
</style>
<slot></slot>
)HTML");
UpdateAllLifecyclePhasesForTest();
{
StyleResolverState state(GetDocument(), *host);
SelectorFilter filter;
MatchResult match_result;
ElementRuleCollector collector(state.ElementContext(), StyleRecalcContext(),
filter, match_result, state.Style(),
EInsideLink::kNotInsideLink);
GetDocument().GetStyleEngine().GetStyleResolver().MatchAllRules(
state, collector, false /* include_smil_properties */);
const auto& properties = match_result.GetMatchedProperties();
ASSERT_EQ(properties.size(), 3u);
// div { display: block }
EXPECT_EQ(properties[0].types_.origin, CascadeOrigin::kUserAgent);
// :host { font-family: myfont }
EXPECT_EQ(match_result.ScopeFromTreeOrder(properties[1].types_.tree_order),
root.GetTreeScope());
EXPECT_EQ(properties[1].types_.origin, CascadeOrigin::kAuthor);
// #host { animation-name: anim }
EXPECT_EQ(properties[2].types_.origin, CascadeOrigin::kAuthor);
EXPECT_EQ(match_result.ScopeFromTreeOrder(properties[2].types_.tree_order),
host->GetTreeScope());
}
{
auto* span = GetDocument().getElementById("slotted");
StyleResolverState state(GetDocument(), *span);
SelectorFilter filter;
MatchResult match_result;
ElementRuleCollector collector(state.ElementContext(), StyleRecalcContext(),
filter, match_result, state.Style(),
EInsideLink::kNotInsideLink);
GetDocument().GetStyleEngine().GetStyleResolver().MatchAllRules(
state, collector, false /* include_smil_properties */);
const auto& properties = match_result.GetMatchedProperties();
ASSERT_EQ(properties.size(), 2u);
// ::slotted(span) { animation-name: anim-inner-slotted }
EXPECT_EQ(properties[0].types_.origin, CascadeOrigin::kAuthor);
EXPECT_EQ(match_result.ScopeFromTreeOrder(properties[0].types_.tree_order),
inner_root.GetTreeScope());
// ::slotted(span) { animation-name: anim-slotted }
EXPECT_EQ(properties[1].types_.origin, CascadeOrigin::kAuthor);
EXPECT_EQ(match_result.ScopeFromTreeOrder(properties[1].types_.tree_order),
root.GetTreeScope());
}
}
TEST_F(StyleResolverTest, InheritStyleImagesFromDisplayContents) {
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
#parent {
display: contents;
background-image: url(1.png);
border-image-source: url(2.png);
cursor: url(3.ico), text;
list-style-image: url(4.png);
shape-outside: url(5.png);
-webkit-box-reflect: below 0 url(6.png);
-webkit-mask-box-image-source: url(7.png);
-webkit-mask-image: url(8.png);
}
#child {
background-image: inherit;
border-image-source: inherit;
cursor: inherit;
list-style-image: inherit;
shape-outside: inherit;
-webkit-box-reflect: inherit;
-webkit-mask-box-image-source: inherit;
-webkit-mask-image: inherit;
}
</style>
<div id="parent">
<div id="child"></div>
</div>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto* child = GetDocument().getElementById("child");
auto* style = child->GetComputedStyle();
ASSERT_TRUE(style);
ASSERT_TRUE(style->BackgroundLayers().GetImage());
EXPECT_FALSE(style->BackgroundLayers().GetImage()->IsPendingImage())
<< "background-image is fetched";
ASSERT_TRUE(style->BorderImageSource());
EXPECT_FALSE(style->BorderImageSource()->IsPendingImage())
<< "border-image-source is fetched";
ASSERT_TRUE(style->Cursors());
ASSERT_TRUE(style->Cursors()->size());
ASSERT_TRUE(style->Cursors()->at(0).GetImage());
EXPECT_FALSE(style->Cursors()->at(0).GetImage()->IsPendingImage())
<< "cursor is fetched";
ASSERT_TRUE(style->ListStyleImage());
EXPECT_FALSE(style->ListStyleImage()->IsPendingImage())
<< "list-style-image is fetched";
ASSERT_TRUE(style->ShapeOutside());
ASSERT_TRUE(style->ShapeOutside()->GetImage());
EXPECT_FALSE(style->ShapeOutside()->GetImage()->IsPendingImage())
<< "shape-outside is fetched";
ASSERT_TRUE(style->BoxReflect());
ASSERT_TRUE(style->BoxReflect()->Mask().GetImage());
EXPECT_FALSE(style->BoxReflect()->Mask().GetImage()->IsPendingImage())
<< "-webkit-box-reflect is fetched";
ASSERT_TRUE(style->MaskBoxImageSource());
EXPECT_FALSE(style->MaskBoxImageSource()->IsPendingImage())
<< "-webkit-mask-box-image-source";
ASSERT_TRUE(style->MaskImage());
EXPECT_FALSE(style->MaskImage()->IsPendingImage())
<< "-webkit-mask-image is fetched";
}
TEST_F(StyleResolverTest, DependsOnContainerQueries) {
ScopedCSSContainerQueriesForTest scoped_feature(true);
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
#a { color: red; }
@container (min-width: 0px) {
#b { color: blue; }
span { color: green; }
#d { color: coral; }
}
</style>
<div id=a></div>
<span id=b></span>
<span id=c></span>
<div id=d></div>
<div id=e></div>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto* a = GetDocument().getElementById("a");
auto* b = GetDocument().getElementById("b");
auto* c = GetDocument().getElementById("c");
auto* d = GetDocument().getElementById("d");
auto* e = GetDocument().getElementById("e");
ASSERT_TRUE(a);
ASSERT_TRUE(b);
ASSERT_TRUE(c);
ASSERT_TRUE(d);
ASSERT_TRUE(e);
EXPECT_FALSE(a->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_TRUE(b->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_TRUE(c->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_TRUE(d->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_FALSE(e->ComputedStyleRef().DependsOnContainerQueries());
}
TEST_F(StyleResolverTest, DependsOnContainerQueriesPseudo) {
ScopedCSSContainerQueriesForTest scoped_feature(true);
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
main { contain: size layout; width: 100px; }
#a::before { content: "before"; }
@container (min-width: 0px) {
#a::after { content: "after"; }
}
</style>
<main>
<div id=a></div>
</main>
)HTML");
UpdateAllLifecyclePhasesForTest();
auto* a = GetDocument().getElementById("a");
auto* before = a->GetPseudoElement(kPseudoIdBefore);
auto* after = a->GetPseudoElement(kPseudoIdAfter);
ASSERT_TRUE(a);
ASSERT_TRUE(before);
ASSERT_TRUE(after);
EXPECT_TRUE(a->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_FALSE(before->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_TRUE(after->ComputedStyleRef().DependsOnContainerQueries());
}
// Verify that the ComputedStyle::DependsOnContainerQuery flag does
// not end up in the MatchedPropertiesCache (MPC).
TEST_F(StyleResolverTest, DependsOnContainerQueriesMPC) {
ScopedCSSContainerQueriesForTest scoped_feature(true);
GetDocument().documentElement()->setInnerHTML(R"HTML(
<style>
@container (min-width: 9999999px) {
#a { color: green; }
}
</style>
<div id=a></div>
<div id=b></div>
)HTML");
// In the above example, both <div id=a> and <div id=b> match the same
// rules (i.e. whatever is provided by UA style). The selector inside
// the @container rule does ultimately _not_ match <div id=a> (because the
// container query evaluates to 'false'), however, it _does_ cause the
// ComputedStyle::DependsOnContainerQuery flag to be set on #a.
//
// We must ensure that we don't add the DependsOnContainerQuery-flagged
// style to the MPC, otherwise the subsequent cache hit for #b would result
// in the flag being (incorrectly) set for that element.
UpdateAllLifecyclePhasesForTest();
auto* a = GetDocument().getElementById("a");
auto* b = GetDocument().getElementById("b");
ASSERT_TRUE(a);
ASSERT_TRUE(b);
EXPECT_TRUE(a->ComputedStyleRef().DependsOnContainerQueries());
EXPECT_FALSE(b->ComputedStyleRef().DependsOnContainerQueries());
}
} // namespace blink