"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/BorderRadiusPercentageResolutionTests.cs b/src/AngleSharp.Css.Tests/Styling/BorderRadiusPercentageResolutionTests.cs
index cdeceba8..87f08cef 100644
--- a/src/AngleSharp.Css.Tests/Styling/BorderRadiusPercentageResolutionTests.cs
+++ b/src/AngleSharp.Css.Tests/Styling/BorderRadiusPercentageResolutionTests.cs
@@ -8,11 +8,9 @@ namespace AngleSharp.Css.Tests.Styling
///
/// Per https://www.w3.org/TR/css-backgrounds-3/#corner-overlap, a percentage `border-radius`
/// resolves its horizontal component against the border box's own *width* and its vertical
- /// component against the border box's own *height* - independently. Confirmed empirically
- /// while building `border-radius` support in a downstream renderer (AngleSharp.Renderer) that
- /// this does not happen: both components resolve against the containing block's/element's
- /// *width* alone, so on a box whose width and height differ, the vertical radius comes out
- /// wrong (tracking the wrong axis's dimension entirely, not merely imprecise).
+ /// component against the border box's own *height* - independently. Since the border box is
+ /// only known during layout, the computed value keeps both components as percentages (see
+ /// `BorderRadiusPercentageComputedValueTests`) instead of resolving them prematurely.
///
[TestFixture]
public class BorderRadiusPercentageResolutionTests
@@ -28,36 +26,32 @@ private static IDocument ParseWithRenderDevice(string html, int viewPortWidth =
}
[Test]
- public void VerticalPercentageComponentResolvesAgainstTheElementsOwnHeight()
+ public void PercentageComponentsOfAnExplicitlySizedBoxStayPercentages()
{
- // 200x100 box, `border-radius: 10% / 30%` - the horizontal component (10%) should
- // resolve against the 200px width (20px); the vertical component (30%) should resolve
- // against the 100px height (30px), not against the 200px width (which would give the
- // wrong value, 60px).
+ // 200x100 box, `border-radius: 10% / 30%` - the computed value is a pair of
+ // values (https://www.w3.org/TR/css-backgrounds-3/#border-radius),
+ // so both components stay percentages. Resolving them (the horizontal one against the
+ // 200px width, the vertical one against the 100px height) is up to layout.
var document = ParseWithRenderDevice("");
var target = document.GetElementById("target");
var style = target.ComputeCurrentStyle();
- var horizontal = style.GetPropertyValue("border-top-left-radius");
- Assert.IsTrue(horizontal.Contains("30px"), $"expected the vertical 30% component to resolve to 30px (30% of the 100px height); got '{horizontal}'");
+ Assert.AreEqual("10% 30%", style.GetPropertyValue("border-top-left-radius"));
}
[Test]
- public void VerticalPercentageComponentTracksHeightAcrossDifferentElementHeights()
+ public void VerticalPercentageComponentIsNotResolvedAgainstTheWidth()
{
- // The same vertical percentage against two different heights (but the same width)
- // must resolve to two different pixel values if it is genuinely tracking height - if
- // it were (incorrectly) tracking width instead, both would resolve identically despite
- // the different heights.
+ // The vertical component must not be turned into a pixel value derived from the
+ // box's width; it is kept as authored, independent of the box's dimensions.
var shortDocument = ParseWithRenderDevice("");
- var shortTarget = shortDocument.GetElementById("target");
- var shortRadius = shortTarget.ComputeCurrentStyle().GetPropertyValue("border-top-left-radius");
+ var shortRadius = shortDocument.GetElementById("target").ComputeCurrentStyle().GetPropertyValue("border-top-left-radius");
var tallDocument = ParseWithRenderDevice("");
- var tallTarget = tallDocument.GetElementById("target");
- var tallRadius = tallTarget.ComputeCurrentStyle().GetPropertyValue("border-top-left-radius");
+ var tallRadius = tallDocument.GetElementById("target").ComputeCurrentStyle().GetPropertyValue("border-top-left-radius");
- Assert.AreNotEqual(shortRadius, tallRadius, "a 30% vertical radius against a 100px-tall box and a 200px-tall box must resolve to different pixel values.");
+ Assert.AreEqual("0px 30%", shortRadius);
+ Assert.AreEqual("0px 30%", tallRadius);
}
}
}
diff --git a/src/AngleSharp.Css.Tests/Styling/Configuration.cs b/src/AngleSharp.Css.Tests/Styling/Configuration.cs
index 0b815069..2ffdc73b 100644
--- a/src/AngleSharp.Css.Tests/Styling/Configuration.cs
+++ b/src/AngleSharp.Css.Tests/Styling/Configuration.cs
@@ -38,7 +38,10 @@ public void ObtainDefaultSheet()
// rule, giving generated-content pseudo-elements their spec-correct default display
// (previously they had none at all, so this renderer's general "unset display defaults
// to block" fallback made every ::before/::after start its own new block line).
- Assert.AreEqual(50, sheet.Rules.Length);
+ // 60, not 50: the UA stylesheet now also carries the rules for elements that are
+ // never rendered (`[hidden]`, `input[type=hidden]`) and the default look of form
+ // controls (text fields, buttons, checkboxes/radios, color and range inputs).
+ Assert.AreEqual(60, sheet.Rules.Length);
}
}
}
diff --git a/src/AngleSharp.Css.Tests/Styling/CssBlendFilterProperties.cs b/src/AngleSharp.Css.Tests/Styling/CssBlendFilterProperties.cs
index 1ce0b04d..8e89d30e 100644
--- a/src/AngleSharp.Css.Tests/Styling/CssBlendFilterProperties.cs
+++ b/src/AngleSharp.Css.Tests/Styling/CssBlendFilterProperties.cs
@@ -229,11 +229,11 @@ public void BackdropFilterNone()
}
[Test]
- public void BackdropFilterInvalid()
+ public void BackdropFilterAcceptsFilterFunctions()
{
var element = _document.CreateElement("div") as IHtmlElement;
element.GetStyle().SetProperty("backdrop-filter", "blur(5px)");
- Assert.AreEqual("", element.GetStyle().GetPropertyValue("backdrop-filter"));
+ Assert.AreEqual("blur(5px)", element.GetStyle().GetPropertyValue("backdrop-filter"));
}
[Test]
diff --git a/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs b/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs
index 0d9a912d..2a0e3e43 100644
--- a/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs
+++ b/src/AngleSharp.Css.Tests/Styling/CustomPropertyCycles.cs
@@ -88,7 +88,7 @@ public void ResolvedTokensAreNotConvertedUsingTheCustomPropertyConverter()
var style = document.QuerySelector("div").ComputeCurrentStyle();
Assert.AreEqual("0", style.GetPropertyValue("--a"));
Assert.AreEqual("0", style.GetPropertyValue("opacity"));
- Assert.AreEqual("0", style.GetPropertyValue("width"));
+ Assert.AreEqual("0px", style.GetPropertyValue("width"));
}
[TestCase("--a:red;--b:var(--a);--c:var(--a);--d:var(--b) var(--c)", "red red")]
@@ -178,7 +178,7 @@ public void OnlyTheWinningDeclarationParticipatesInTheGraph(String text, String
Assert.AreEqual(expected, document.QuerySelector("div").ComputeCurrentStyle().GetPropertyValue("visibility"));
}
- [TestCase("--a:var(--a);margin:var(--a)", "0", "0")]
+ [TestCase("--a:var(--a);margin:var(--a)", "0px", "0px")]
[TestCase("--a:var(--a);margin:var(--a,1px 2px)", "1px", "2px")]
[TestCase("--a:1px 2px;margin:var(--a)", "1px", "2px")]
[TestCase("--a:var(--a);margin:3px var(--a,4px)", "3px", "4px")]
diff --git a/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetFormControlTests.cs b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetFormControlTests.cs
new file mode 100644
index 00000000..6a859b28
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetFormControlTests.cs
@@ -0,0 +1,139 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Dom;
+ using NUnit.Framework;
+ using static CssConstructionFunctions;
+
+ ///
+ /// The default (UA) stylesheet gives form controls `display: inline-block` but none of the
+ /// default look every browser's UA stylesheet gives them (border, padding, background, the
+ /// checkbox/radio margin, the round radio). A downstream renderer (AngleSharp.Renderer) that
+ /// takes every style from the computed style therefore draws an unstyled `<input>` as
+ /// an invisible box, unless it injects that look itself based on tag names - which also makes
+ /// it impossible for an author's own `input { ... }` rules to reliably override it through
+ /// the cascade. The expected values match the look AngleSharp.Renderer used to inject.
+ ///
+ [TestFixture]
+ public class DefaultStyleSheetFormControlTests
+ {
+ private static ICssStyleDeclaration ComputedStyle(string html)
+ {
+ var document = ParseDocument($"{html}");
+ return document.GetElementById("target").ComputeCurrentStyle();
+ }
+
+ private static void AssertBorder(ICssStyleDeclaration style, string width, string lineStyle, string color)
+ {
+ foreach (var side in new[] { "top", "right", "bottom", "left" })
+ {
+ Assert.AreEqual(width, style.GetPropertyValue($"border-{side}-width"), side);
+ Assert.AreEqual(lineStyle, style.GetPropertyValue($"border-{side}-style"), side);
+ Assert.AreEqual(color, style.GetPropertyValue($"border-{side}-color"), side);
+ }
+ }
+
+ private static void AssertPadding(ICssStyleDeclaration style, string vertical, string horizontal)
+ {
+ Assert.AreEqual(vertical, style.GetPropertyValue("padding-top"));
+ Assert.AreEqual(horizontal, style.GetPropertyValue("padding-right"));
+ Assert.AreEqual(vertical, style.GetPropertyValue("padding-bottom"));
+ Assert.AreEqual(horizontal, style.GetPropertyValue("padding-left"));
+ }
+
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ public void TextFieldsHaveABorderPaddingAndAWhiteBackground(string html)
+ {
+ var style = ComputedStyle(html);
+
+ AssertBorder(style, "1px", "solid", "rgba(118, 118, 118, 1)");
+ AssertPadding(style, "2px", "4px");
+ Assert.AreEqual("rgba(255, 255, 255, 1)", style.GetPropertyValue("background-color"));
+ }
+
+ [Test]
+ public void SelectLooksLikeAButtonWithTextPadding()
+ {
+ var style = ComputedStyle("");
+
+ AssertBorder(style, "1px", "solid", "rgba(118, 118, 118, 1)");
+ AssertPadding(style, "2px", "4px");
+ Assert.AreEqual("rgba(232, 232, 232, 1)", style.GetPropertyValue("background-color"));
+ }
+
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("")]
+ public void ButtonsHaveABorderWidePaddingAndAGrayBackground(string html)
+ {
+ var style = ComputedStyle(html);
+
+ AssertBorder(style, "1px", "solid", "rgba(118, 118, 118, 1)");
+ AssertPadding(style, "2px", "10px");
+ Assert.AreEqual("rgba(232, 232, 232, 1)", style.GetPropertyValue("background-color"));
+ }
+
+ [Test]
+ public void ColorInputHasANarrowPadding()
+ {
+ var style = ComputedStyle("");
+
+ AssertBorder(style, "1px", "solid", "rgba(118, 118, 118, 1)");
+ AssertPadding(style, "2px", "2px");
+ Assert.AreEqual("rgba(255, 255, 255, 1)", style.GetPropertyValue("background-color"));
+ }
+
+ [TestCase("checkbox")]
+ [TestCase("radio")]
+ public void CheckboxesAndRadiosHaveABorderAndTheirOwnMargin(string type)
+ {
+ var style = ComputedStyle($"");
+
+ AssertBorder(style, "1px", "solid", "rgba(118, 118, 118, 1)");
+ AssertPadding(style, "0px", "0px");
+ Assert.AreEqual("rgba(255, 255, 255, 1)", style.GetPropertyValue("background-color"));
+ Assert.AreEqual("3px", style.GetPropertyValue("margin-top"));
+ Assert.AreEqual("3px", style.GetPropertyValue("margin-right"));
+ Assert.AreEqual("3px", style.GetPropertyValue("margin-bottom"));
+ Assert.AreEqual("4px", style.GetPropertyValue("margin-left"));
+ }
+
+ [Test]
+ public void RadioIsRound()
+ {
+ var style = ComputedStyle("");
+ var radius = style.GetPropertyValue("border-top-left-radius");
+
+ Assert.IsNotEmpty(radius);
+ Assert.AreNotEqual("0px", radius);
+ }
+
+ [Test]
+ public void RangeHasNoBoxOfItsOwn()
+ {
+ var style = ComputedStyle("");
+
+ Assert.AreEqual("none", style.GetPropertyValue("border-top-style"));
+ Assert.AreEqual("rgba(0, 0, 0, 0)", style.GetPropertyValue("background-color"));
+ }
+
+ [Test]
+ public void AuthorRulesOverrideTheFormControlLook()
+ {
+ var document = ParseDocument("");
+ var style = document.GetElementById("target").ComputeCurrentStyle();
+
+ AssertBorder(style, "3px", "dashed", "rgba(255, 0, 0, 1)");
+ AssertPadding(style, "1px", "1px");
+ Assert.AreEqual("rgba(0, 0, 0, 1)", style.GetPropertyValue("background-color"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetHtmlElementsTests.cs b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetHtmlElementsTests.cs
new file mode 100644
index 00000000..08c739df
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetHtmlElementsTests.cs
@@ -0,0 +1,60 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Dom;
+ using NUnit.Framework;
+ using static CssConstructionFunctions;
+
+ ///
+ /// The default (UA) stylesheet is the CSS 2.2 sample stylesheet, which predates HTML5: it has
+ /// no rules for the sectioning and grouping elements, the elements that are never rendered,
+ /// or form-control text handling that the HTML Standard's rendering section
+ /// (https://html.spec.whatwg.org/multipage/rendering.html) requires. A downstream renderer
+ /// (AngleSharp.Renderer) that takes every style from the computed style therefore lays out a
+ /// `<section>` or `<footer>` as `inline` (the initial value of `display`) and
+ /// would render the text of a `<script>` in the body, unless it guesses from tag names.
+ ///
+ [TestFixture]
+ public class DefaultStyleSheetHtmlElementsTests
+ {
+ private static string ComputedValue(string html, string property)
+ {
+ var document = ParseDocument(html);
+ return document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property);
+ }
+
+ [TestCase("article")]
+ [TestCase("aside")]
+ [TestCase("details")]
+ [TestCase("figcaption")]
+ [TestCase("figure")]
+ [TestCase("footer")]
+ [TestCase("header")]
+ [TestCase("hgroup")]
+ [TestCase("main")]
+ [TestCase("nav")]
+ [TestCase("search")]
+ [TestCase("section")]
+ [TestCase("legend")]
+ public void Html5BlockElementsAreDisplayBlock(string tagName)
+ {
+ Assert.AreEqual("block", ComputedValue($"<{tagName} id=target>x{tagName}>", "display"));
+ }
+
+ [TestCase("")]
+ [TestCase("")]
+ [TestCase("
x
")]
+ [TestCase("
x
")]
+ [TestCase("")]
+ public void NonRenderedElementsAreDisplayNone(string html)
+ {
+ Assert.AreEqual("none", ComputedValue(html, "display"));
+ }
+
+ [Test]
+ public void TextareaPreservesWhiteSpace()
+ {
+ Assert.AreEqual("pre-wrap", ComputedValue("", "white-space"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/DisplayFlowRootTests.cs b/src/AngleSharp.Css.Tests/Styling/DisplayFlowRootTests.cs
new file mode 100644
index 00000000..5266b98a
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/DisplayFlowRootTests.cs
@@ -0,0 +1,37 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Dom;
+ using NUnit.Framework;
+ using static CssConstructionFunctions;
+
+ ///
+ /// `display: flow-root` (CSS Display 3) is a block box that establishes a new block formatting
+ /// context - the standard way to make a container enclose its floats. The value is not in the
+ /// list of recognized `display` keywords, so the declaration is dropped and the element keeps
+ /// whatever `display` the rest of the cascade gives it (`block` for a `div`). Found in a downstream renderer (AngleSharp.Renderer) while implementing
+ /// floats: a `flow-root` container did not grow to contain its floated children.
+ ///
+ [TestFixture]
+ public class DisplayFlowRootTests
+ {
+ [Test]
+ public void FlowRootIsAValidDisplayValue()
+ {
+ var document = ParseDocument("");
+ var target = document.GetElementById("target");
+
+ Assert.AreEqual("flow-root", target.ComputeCurrentStyle().GetPropertyValue("display"));
+ }
+
+ [Test]
+ public void FlowRootIsKeptOnTheDeclaration()
+ {
+ var document = ParseDocument("");
+ var target = document.GetElementById("target");
+
+ Assert.AreEqual("flow-root", target.GetStyle().GetPropertyValue("display"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/FontWeightComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/FontWeightComputedStyleTests.cs
new file mode 100644
index 00000000..8ff1f1d5
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/FontWeightComputedStyleTests.cs
@@ -0,0 +1,81 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Dom;
+ using NUnit.Framework;
+ using static CssConstructionFunctions;
+
+ ///
+ /// The computed value of `font-weight` is a number: the relative keywords `bolder` and
+ /// `lighter` resolve against the parent's computed weight (CSS Fonts 4, section 2.2.1), and
+ /// descendants inherit that number, not the keyword. Found while rendering `<b>` and
+ /// `<strong>` (whose UA rule is `font-weight: bolder`) in a downstream renderer
+ /// (AngleSharp.Renderer): the computed style reports the literal keyword `bolder` for the
+ /// element itself and for every descendant, so a consumer cannot know the actual weight
+ /// without re-implementing the cascade, and a nested element applies `bolder` once more.
+ ///
+ [TestFixture]
+ public class FontWeightComputedStyleTests
+ {
+ private static string ComputedFontWeight(string html, string id)
+ {
+ var document = ParseDocument(html);
+ return document.GetElementById(id).ComputeCurrentStyle().GetPropertyValue("font-weight");
+ }
+
+ [Test]
+ public void UaBolderOnBoldElementResolvesAgainstNormalParent()
+ {
+ Assert.AreEqual("700", ComputedFontWeight("
", "target"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs b/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs
index d95101a4..f5e2810d 100644
--- a/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs
+++ b/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs
@@ -467,7 +467,8 @@ public void DefaultStyleSheetTest_Issue21()
var htmlParser = browsingContext.GetService();
var document = htmlParser.ParseDocument("Hello, World!");
var boldStyle = document.Body.FirstElementChild.ComputeCurrentStyle();
- Assert.AreEqual("bolder", boldStyle.GetFontWeight());
+ // The computed value of `bolder` is resolved against the parent's weight (normal).
+ Assert.AreEqual("700", boldStyle.GetFontWeight());
}
[Test]
diff --git a/src/AngleSharp.Css.Tests/Styling/PaintPropertiesComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/PaintPropertiesComputedStyleTests.cs
new file mode 100644
index 00000000..05798278
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/PaintPropertiesComputedStyleTests.cs
@@ -0,0 +1,96 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Dom;
+ using AngleSharp.Html.Parser;
+ using NUnit.Framework;
+
+ ///
+ /// Computed values of paint-related properties that a downstream renderer
+ /// (AngleSharp.Renderer) needs while implementing `border-image`, CSS masks, `backdrop-filter`
+ /// and multiple background layers:
+ /// - `border-image-slice` serializes a single number as `30 NaNpx NaNpx NaNpx`.
+ /// - A unitless `border-image-width`/`border-image-outset` (a multiple of the border width)
+ /// throws while computing, which aborts the whole render tree.
+ /// - `backdrop-filter`, `mask-size` and `mask-repeat` are dropped at parse time.
+ /// - `mask-position` is resolved to pixels against the viewport; like `background-position`,
+ /// its percentages refer to the element's own box and must stay percentages.
+ /// - `background-repeat` and `background-position` with more than one layer are parsed but
+ /// computed as empty, unlike `background-size`, `background-clip` or `background-origin`.
+ ///
+ [TestFixture]
+ public class PaintPropertiesComputedStyleTests
+ {
+ private static ICssStyleDeclaration ComputedStyle(string style)
+ {
+ var config = Configuration.Default
+ .WithCss()
+ .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 });
+ var parser = BrowsingContext.New(config).GetService();
+ var document = parser.ParseDocument($"
x
");
+ return document.GetElementById("target").ComputeCurrentStyle();
+ }
+
+ [TestCase("30", "30")]
+ [TestCase("30%", "30%")]
+ [TestCase("10 20 30 40", "10 20 30 40")]
+ [TestCase("25% fill", "25% fill")]
+ public void BorderImageSliceKeepsItsValues(string slice, string expected)
+ {
+ Assert.AreEqual(expected, ComputedStyle($"border-image-slice: {slice}").GetPropertyValue("border-image-slice"));
+ }
+
+ [TestCase("border-image-width: 2", "border-image-width", "2")]
+ [TestCase("border-image-width: 1 8px", "border-image-width", "1 8px")]
+ [TestCase("border-image-outset: 1.5", "border-image-outset", "1.5")]
+ [TestCase("border-image: linear-gradient(red, blue) 25% / 1 / 4px round", "border-image-width", "1")]
+ public void BorderImageNumbersAreMultiplesOfTheBorderWidth(string style, string property, string expected)
+ {
+ // A unitless number is not a length: computing it currently throws "Unsupported unit
+ // cannot be converted", which aborts the whole render tree.
+ Assert.AreEqual(expected, ComputedStyle(style).GetPropertyValue(property));
+ }
+
+ [Test]
+ public void BackdropFilterIsComputed()
+ {
+ Assert.AreEqual("blur(4px) grayscale(50%)", ComputedStyle("backdrop-filter: blur(4px) grayscale(50%)").GetPropertyValue("backdrop-filter"));
+ }
+
+ [Test]
+ public void MaskSizeIsComputed()
+ {
+ Assert.AreEqual("20px 30px, cover", ComputedStyle("mask-image: none, none; mask-size: 20px 30px, cover").GetPropertyValue("mask-size"));
+ }
+
+ [Test]
+ public void MaskRepeatIsComputed()
+ {
+ Assert.AreEqual("no-repeat, space", ComputedStyle("mask-image: none, none; mask-repeat: no-repeat, space").GetPropertyValue("mask-repeat"));
+ }
+
+ [Test]
+ public void MaskPositionPercentagesAreNotResolvedAgainstTheViewport()
+ {
+ var position = ComputedStyle("mask-image: none; mask-position: center").GetPropertyValue("mask-position");
+
+ Assert.That(position, Does.Not.Contain("px"));
+ }
+
+ [Test]
+ public void BackgroundRepeatKeepsEveryLayer()
+ {
+ Assert.AreEqual("space round, no-repeat", ComputedStyle("background-image: none, none; background-repeat: space round, no-repeat").GetPropertyValue("background-repeat"));
+ }
+
+ [Test]
+ public void BackgroundPositionKeepsEveryLayer()
+ {
+ var position = ComputedStyle("background-image: none, none; background-position: left top, 30% 70%").GetPropertyValue("background-position");
+
+ Assert.AreEqual(2, position.Split(',').Length, position);
+ Assert.That(position, Does.Contain("30% 70%"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/PseudoElementComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/PseudoElementComputedStyleTests.cs
new file mode 100644
index 00000000..ddda6319
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/PseudoElementComputedStyleTests.cs
@@ -0,0 +1,61 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Dom;
+ using NUnit.Framework;
+ using static CssConstructionFunctions;
+
+ ///
+ /// A `::before`/`::after` pseudo-element only gets the declarations of rules whose selector
+ /// targets that pseudo-element, plus whatever it inherits from its host. A rule that matches
+ /// the host itself (`div#e { border: ... }`, or the default stylesheet's `input { ... }`) must
+ /// not apply to the host's pseudo-elements. Found in a downstream renderer
+ /// (AngleSharp.Renderer): a `::before` on an `<input>` got the input's own border,
+ /// padding and background, so the control's look was painted twice.
+ ///
+ [TestFixture]
+ public class PseudoElementComputedStyleTests
+ {
+ private static ICssStyleDeclaration PseudoStyle(string css, string pseudo)
+ {
+ var document = ParseDocument($"
text
");
+ var window = document.DefaultView;
+ return window.GetComputedStyle(document.GetElementById("host"), pseudo);
+ }
+
+ [Test]
+ public void HostRuleDoesNotApplyNonInheritedPropertiesToBefore()
+ {
+ var style = PseudoStyle("div#host { border: 2px solid red; padding: 5px; background-color: blue } #host::before { content: 'x' }", "::before");
+
+ Assert.AreNotEqual("2px", style.GetPropertyValue("border-top-width"));
+ Assert.AreNotEqual("5px", style.GetPropertyValue("padding-left"));
+ Assert.AreNotEqual("rgba(0, 0, 255, 1)", style.GetPropertyValue("background-color"));
+ }
+
+ [Test]
+ public void HostRuleDoesNotApplyNonInheritedPropertiesToAfter()
+ {
+ var style = PseudoStyle("div { margin-left: 30px } div::after { content: 'x' }", "::after");
+
+ Assert.AreNotEqual("30px", style.GetPropertyValue("margin-left"));
+ }
+
+ [Test]
+ public void InheritedPropertiesOfTheHostStillReachThePseudoElement()
+ {
+ var style = PseudoStyle("div#host { color: red } #host::before { content: 'x' }", "::before");
+
+ Assert.AreEqual("rgba(255, 0, 0, 1)", style.GetPropertyValue("color"));
+ }
+
+ [Test]
+ public void RulesTargetingThePseudoElementStillApply()
+ {
+ var style = PseudoStyle("#host::before { content: 'x'; padding-left: 7px }", "::before");
+
+ Assert.AreEqual("7px", style.GetPropertyValue("padding-left"));
+ }
+ }
+}
diff --git a/src/AngleSharp.Css.Tests/Styling/TextPropertiesComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/TextPropertiesComputedStyleTests.cs
new file mode 100644
index 00000000..8b6936b2
--- /dev/null
+++ b/src/AngleSharp.Css.Tests/Styling/TextPropertiesComputedStyleTests.cs
@@ -0,0 +1,64 @@
+#nullable disable
+namespace AngleSharp.Css.Tests.Styling
+{
+ using AngleSharp.Css.Dom;
+ using AngleSharp.Dom;
+ using AngleSharp.Html.Parser;
+ using NUnit.Framework;
+
+ ///
+ /// Computed values of text properties a downstream renderer (AngleSharp.Renderer) reads while
+ /// implementing small caps, font stretch and justification:
+ /// - `font-variant-caps` is not a known property, so `all-small-caps` is dropped and the
+ /// `font-variant` shorthand does not set it.
+ /// - `font-stretch` drops percentage values (`75%`).
+ /// - `text-align-last` is not inherited, unlike what CSS Text 3 specifies.
+ ///
+ [TestFixture]
+ public class TextPropertiesComputedStyleTests
+ {
+ private static ICssStyleDeclaration ComputedStyle(string html, string id = "target")
+ {
+ var config = Configuration.Default
+ .WithCss()
+ .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 });
+ var parser = BrowsingContext.New(config).GetService();
+ var document = parser.ParseDocument(html);
+ return document.GetElementById(id).ComputeCurrentStyle();
+ }
+
+ [TestCase("small-caps")]
+ [TestCase("all-small-caps")]
+ [TestCase("petite-caps")]
+ [TestCase("unicase")]
+ public void FontVariantCapsIsComputed(string value)
+ {
+ Assert.AreEqual(value, ComputedStyle($"