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