From d4f683f86fd016b0ad0b8dc3935df08d6b227416 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 20 Jun 2026 19:14:14 +1000 Subject: [PATCH] fix: handle null inline style declarations in style attribute comparers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StyleAttributeComparer and OrderingStyleAttributeComparer passed the result of IElement.GetStyle() straight into CompareCssStyleDeclarations, which dereferences it. GetStyle() returns null when an element has no inline CSS style declaration to parse — a non-HTML (SVG/MathML) element, or any element when the browsing context has no ICssParser registered (e.g. consumers that don't call .WithCss(), such as bUnit). Comparing a styled element in that situation threw a NullReferenceException. Both comparers now fall back to comparing the raw style attribute value when either declaration is null, so such elements compare by value instead of throwing. Adds regression tests that build the comparison in a CSS-less browsing context, since the shared test fixture enables CSS and so cannot reproduce the issue. --- CHANGELOG.md | 6 ++- .../CssLessComparisonFactory.cs | 38 +++++++++++++++++++ .../OrderingStyleAttributeComparerTest.cs | 15 ++++++++ .../StyleAttributeComparerTest.cs | 15 ++++++++ .../OrderingStyleAttributeComparer.cs | 11 +++++- .../StyleAttributeComparer.cs | 11 +++++- 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/CssLessComparisonFactory.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fd409a8..f26c3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ +# 1.1.2 + +- Fixed `StyleAttributeComparer` and `OrderingStyleAttributeComparer` throwing a `NullReferenceException` when comparing a `style` attribute on an element that has no inline CSS style declaration — e.g. a non-HTML (SVG/MathML) element, or any element when the browsing context has no CSS parser registered. Such `style` attributes are now compared by their raw value. + # 1.1.1 -- Fixed marking comaprison results in the pipeline so that strategies down the line see if there is a diff. By [@egil](https://github.com/egil) and [@linkdotnet](https://github.com/linkdotnet). +- Fixed marking comparison results in the pipeline so that strategies down the line see if there is a diff. By [@egil](https://github.com/egil) and [@linkdotnet](https://github.com/linkdotnet). # 1.1.0 diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/CssLessComparisonFactory.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/CssLessComparisonFactory.cs new file mode 100644 index 0000000..c7fcc43 --- /dev/null +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/CssLessComparisonFactory.cs @@ -0,0 +1,38 @@ +using AngleSharp.Html.Parser; + +namespace AngleSharp.Diffing.Strategies.AttributeStrategies; + +/// +/// Builds style-attribute comparisons whose elements are parsed in a browsing context without CSS support. +/// With no registered, IElement.GetStyle() returns +/// null — the situation CSS-less consumers such as bUnit hit when diffing markup that contains inline +/// SVG. The shared enables CSS, so it cannot reproduce this on its own. +/// +internal static class CssLessComparisonFactory +{ + public static AttributeComparison ToStyleAttributeComparison(string controlHtml, string testHtml) + { + // Same parser setup as DiffingTestFixture, but deliberately without .WithCss(). + var config = Configuration.Default + .With(_ => + new HtmlParser( + new() + { + IsKeepingSourceReferences = true + }, + _)); + var context = BrowsingContext.New(config); + var parser = context.GetService()!; + var document = context.OpenNewAsync().Result; + + return new AttributeComparison( + ToStyleSource(ComparisonSourceType.Control, controlHtml), + ToStyleSource(ComparisonSourceType.Test, testHtml)); + + AttributeComparisonSource ToStyleSource(ComparisonSourceType sourceType, string html) + { + var element = parser.ParseFragment(html, document.Body!)[0]; + return new("style", element.ToComparisonSource(0, sourceType)); + } + } +} diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs index 6e54c4b..0803894 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs @@ -71,4 +71,19 @@ public void Test006(string control, string test) var comparison = ToAttributeComparison(control, "style", test, "style"); OrderingStyleAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); } + + [Theory(DisplayName = "Style comparison falls back to the raw value when an element has no CSS style declaration (e.g. inline SVG)")] + [InlineData(@"", @"", true)] + [InlineData(@"", @"", false)] + public void Test007(string control, string test, bool expectedSame) + { + // GetStyle() returns null when the browsing context has no CSS parser; the comparer must not throw. + var comparison = CssLessComparisonFactory.ToStyleAttributeComparison(control, test); + + var expected = expectedSame + ? CompareResult.Same + : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value)); + + OrderingStyleAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(expected); + } } diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs index 1595920..16cde50 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs @@ -63,4 +63,19 @@ public void Test005(string control, string test) var comparison = ToAttributeComparison(control, "style", test, "style"); StyleAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); } + + [Theory(DisplayName = "Style comparison falls back to the raw value when an element has no CSS style declaration (e.g. inline SVG)")] + [InlineData(@"", @"", true)] + [InlineData(@"", @"", false)] + public void Test006(string control, string test, bool expectedSame) + { + // GetStyle() returns null when the browsing context has no CSS parser; the comparer must not throw. + var comparison = CssLessComparisonFactory.ToStyleAttributeComparison(control, test); + + var expected = expectedSame + ? CompareResult.Same + : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value)); + + StyleAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(expected); + } } diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs index 19fcb5e..8e365ff 100644 --- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs +++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs @@ -29,7 +29,16 @@ private static CompareResult CompareElementStyle(in AttributeComparison comparis var (ctrlElm, testElm) = comparison.AttributeElements; var ctrlStyle = ctrlElm.GetStyle(); var testStyle = testElm.GetStyle(); - return CompareCssStyleDeclarations(ctrlStyle, testStyle) + + // GetStyle() returns null when an element exposes no inline CSS style declaration — e.g. a non-HTML + // (SVG/MathML) element, or any element when the browsing context has no CSS parser registered. Fall + // back to comparing the raw style attribute values in that case, so such elements are compared by + // value instead of throwing a NullReferenceException. + var areEqual = ctrlStyle is not null && testStyle is not null + ? CompareCssStyleDeclarations(ctrlStyle, testStyle) + : string.Equals(comparison.Control.Attribute.Value, comparison.Test.Attribute.Value, StringComparison.Ordinal); + + return areEqual ? CompareResult.Same : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value)); } diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs index 8b41cdf..5e72356 100644 --- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs +++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs @@ -29,7 +29,16 @@ private static CompareResult CompareElementStyle(in AttributeComparison comparis var (ctrlElm, testElm) = comparison.AttributeElements; var ctrlStyle = ctrlElm.GetStyle(); var testStyle = testElm.GetStyle(); - return CompareCssStyleDeclarations(ctrlStyle, testStyle) + + // GetStyle() returns null when an element exposes no inline CSS style declaration — e.g. a non-HTML + // (SVG/MathML) element, or any element when the browsing context has no CSS parser registered. Fall + // back to comparing the raw style attribute values in that case, so such elements are compared by + // value instead of throwing a NullReferenceException. + var areEqual = ctrlStyle is not null && testStyle is not null + ? CompareCssStyleDeclarations(ctrlStyle, testStyle) + : string.Equals(comparison.Control.Attribute.Value, comparison.Test.Attribute.Value, StringComparison.Ordinal); + + return areEqual ? CompareResult.Same : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value)); }