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)); }