Skip to content

Commit 910674e

Browse files
authored
Merge pull request #972 from bUnit-dev/release/v1.16
Release of new minor version v1.16
2 parents 1c969be + d5814ae commit 910674e

File tree

8 files changed

+158
-27
lines changed

8 files changed

+158
-27
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
- Changed semantic comparer to handle elements parsed outside their proper context, e.g. an `<path>` element parsed without being inside a `<svg>` element. The semantic comparer will now be able to treat those as regular elements and thus be able to compare correctly to other elements of the same type and with the same node name. By [@egil](https://github.com/egil).
10+
911
## [1.15.5] - 2023-02-04
1012

11-
- Upgrade AngleSharp.Diffing to 0.17.1
13+
- Upgrade AngleSharp.Diffing to 0.17.1.
1214

1315
## [1.14.4] - 2023-01-11
1416

src/bunit.web/Diffing/BlazorDiffingHelpers.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using AngleSharp.Diffing.Core;
2+
using AngleSharp.Html.Dom;
23

34
namespace Bunit.Diffing;
45

@@ -20,4 +21,41 @@ public static FilterDecision BlazorAttributeFilter(in AttributeComparisonSource
2021

2122
return currentDecision;
2223
}
24+
25+
// Based on the forward searching node matcher in AngleSharp.Diffing,
26+
// but this also tries to match nodes that are IHtmlUnknownElement
27+
// but have the same NodeType and NodeName. This allow us to
28+
// deal with SVG elements that are parsed without the proper context,
29+
// i.e. inside an <SVG> element.
30+
internal static IEnumerable<Comparison> UnknownElementMatch(
31+
IDiffContext context,
32+
SourceCollection controlSources,
33+
SourceCollection testSources)
34+
{
35+
var lastMatchedTestNodeIndex = -1;
36+
foreach (var control in controlSources.GetUnmatched())
37+
{
38+
var comparison = TryFindMatchingNodes(control, testSources, lastMatchedTestNodeIndex + 1);
39+
if (comparison.HasValue)
40+
{
41+
yield return comparison.Value;
42+
lastMatchedTestNodeIndex = comparison.Value.Test.Index;
43+
}
44+
}
45+
}
46+
47+
private static Comparison? TryFindMatchingNodes(in ComparisonSource control, SourceCollection testSources, int startIndex)
48+
{
49+
foreach (var test in testSources.GetUnmatched(startIndex))
50+
{
51+
if (control.Node is IHtmlUnknownElement
52+
|| test.Node is IHtmlUnknownElement
53+
&& control.Node.NodeType == test.Node.NodeType
54+
&& control.Node.NodeName.Equals(test.Node.NodeName, StringComparison.OrdinalIgnoreCase))
55+
{
56+
return new Comparison(control, test);
57+
}
58+
}
59+
return null;
60+
}
2361
}

src/bunit.web/Diffing/HtmlComparer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public HtmlComparer()
2121
{
2222
var strategy = new DiffingStrategyPipeline();
2323
strategy.AddDefaultOptions();
24+
strategy.AddMatcher(BlazorDiffingHelpers.UnknownElementMatch, StrategyType.Specialized);
2425
strategy.AddFilter(BlazorDiffingHelpers.BlazorAttributeFilter, StrategyType.Specialized);
2526
differenceEngine = new HtmlDiffer(strategy);
2627
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<svg><path></path></svg>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using Xunit;
5+
6+
namespace Bunit.TestAssets.XunitExtensions;
7+
8+
public static class TheoryDataExtensions
9+
{
10+
public static TheoryData<T> Clone<T>(this TheoryData<T> existing)
11+
{
12+
var result = new TheoryData<T>();
13+
foreach (var item in existing)
14+
{
15+
result.Add((T)item[0]);
16+
}
17+
return result;
18+
}
19+
20+
public static TheoryData<T> AddRange<T>(this TheoryData<T> theoryData, IEnumerable<T> items)
21+
{
22+
foreach (var item in items)
23+
{
24+
theoryData.Add(item);
25+
}
26+
return theoryData;
27+
}
28+
29+
public static TheoryData<T> AddRange<T>(this TheoryData<T> theoryData, params T[] items)
30+
{
31+
foreach (var item in items)
32+
{
33+
theoryData.Add(item);
34+
}
35+
return theoryData;
36+
}
37+
}

tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,18 @@ public void Test014()
123123

124124
cut.Find("div").MarkupMatches(cut.FindAll("div"));
125125
}
126+
127+
[Fact(DisplayName = "Handles HtmlUnknownElement when comparing elements")]
128+
public void Test015()
129+
{
130+
var chart = RenderComponent<SimpleSvg>();
131+
132+
// the path will be returned as a SvgElement since it is
133+
// parsed in the context of a <svg> element.
134+
var path = chart.Find("path");
135+
136+
// path will be parsed as an HtmlUnknownElement because it is not
137+
// in a known context (e.g. <svg> or <foreignObject>)
138+
path.MarkupMatches("<path />");
139+
}
126140
}

tests/bunit.web.tests/Rendering/BunitHtmlParserTest.cs

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using Bunit.TestAssets.XunitExtensions;
12
using AngleSharp.Dom;
3+
using AngleSharp.Html.Dom;
24

35
namespace Bunit.Rendering;
46

@@ -9,26 +11,47 @@ public class BunitHtmlParserTest
911
/// <summary>
1012
/// All HTML5 elements according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element.
1113
/// </summary>
12-
public static readonly IEnumerable<object[]> BodyHtmlElements = new[]
14+
public static readonly TheoryData<string> BodyHtmlElements = new TheoryData<string>
1315
{
14-
"base", "link", "meta", "style", "title",
15-
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "main", "nav", "section",
16-
"blockquote", "dd", "div", "dl", "dt", "figcaption", "figure", "hr", "li", "ol", "p", "pre", "ul",
17-
"a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn", "em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr",
18-
"area", "audio", "img", "map", "track", "video",
19-
"embed", "iframe", "object", "param", "picture", "source",
20-
"canvas", "noscript", "script",
21-
"del", "ins",
22-
"caption", "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr",
23-
"button", "datalist", "fieldset", "form", "input", "label", "legend", "meter", "optgroup", "option", "output", "progress", "select", "textarea",
24-
"details", "dialog", "menu", "summary",
25-
"slot", "template",
26-
"acronym", "applet", "basefont", "bgsound", "big", "blink", "center", "command", "content", "dir", "element", "font", "keygen", "listing", "marquee", "menuitem", "multicol", "nextid", "nobr", "noembed", "noframes", "plaintext", "shadow", "spacer", "strike", "tt", "xmp",
27-
// "frame","frameset","image","isindex" // not supported
28-
}.Select(x => new[] { x });
29-
30-
public static readonly IEnumerable<object[]> BodyHtmlAndSpecialElements = BodyHtmlElements.Concat(
31-
new[] { "html", "head", "body", }.Select(x => new[] { x }));
16+
"base", "link", "meta", "style", "title",
17+
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "main", "nav", "section",
18+
"blockquote", "dd", "div", "dl", "dt", "figcaption", "figure", "hr", "li", "ol", "p", "pre", "ul",
19+
"a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn", "em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr",
20+
"area", "audio", "img", "map", "track", "video", "svg",
21+
"embed", "iframe", "object", "param", "picture", "source",
22+
"canvas", "noscript", "script",
23+
"del", "ins",
24+
"caption", "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr",
25+
"button", "datalist", "fieldset", "form", "input", "label", "legend", "meter", "optgroup", "option", "output", "progress", "select", "textarea",
26+
"details", "dialog", "menu", "summary",
27+
"slot", "template",
28+
"acronym", "applet", "basefont", "bgsound", "big", "blink", "center", "command", "content", "dir", "element", "font", "keygen", "listing", "marquee", "menuitem", "multicol", "nextid", "nobr", "noembed", "noframes", "plaintext", "shadow", "spacer", "strike", "tt", "xmp",
29+
// "frame","frameset","image","isindex" // not supported
30+
};
31+
32+
public static readonly IEnumerable<object[]> BodyHtmlAndSpecialElements = BodyHtmlElements
33+
.Clone()
34+
.AddRange("html", "head", "body");
35+
36+
public static readonly TheoryData<string> SvgElements = new TheoryData<string>
37+
{
38+
"animate", "animateMotion", "animateTransform",
39+
"circle", "clipPath",
40+
"defs", "desc", "discard",
41+
"ellipse",
42+
"feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject",
43+
"g",
44+
"hatch", "hatchpath",
45+
"line", "linearGradient",
46+
"marker", "mask", "metadata", "mpath",
47+
"path", "pattern", "polygon", "polyline",
48+
"radialGradient", "rect",
49+
"set", "stop", "switch", "symbol",
50+
"text", "textPath", "tspan",
51+
"use",
52+
"view"
53+
// "a", "image", "script", "svg", "title", "style" removed since they overlap with HTML elements
54+
};
3255

3356
[Fact(DisplayName = "Parse() called with null")]
3457
public void ParseCalledWithNull()
@@ -46,7 +69,7 @@ public void ParseWithWhitespaceOnly(string text)
4669
actual.ShouldHaveSingleItem().TextContent.ShouldBe(text);
4770
}
4871

49-
[Theory(DisplayName = "Parse() passed <TAG id=TAG>")]
72+
[Theory(DisplayName = "Parse() parses <TAG id=TAG>")]
5073
[MemberData(nameof(BodyHtmlAndSpecialElements))]
5174
public void Test001(string elementName)
5275
{
@@ -55,7 +78,7 @@ public void Test001(string elementName)
5578
VerifyElementParsedWithId(elementName, actual);
5679
}
5780

58-
[Theory(DisplayName = "Parse() passed <TAG> with whitespace before")]
81+
[Theory(DisplayName = "Parse() parses <TAG> with whitespace before")]
5982
[MemberData(nameof(BodyHtmlElements))]
6083
public void Test002(string elementName)
6184
{
@@ -64,7 +87,7 @@ public void Test002(string elementName)
6487
VerifyElementParsedWithId(elementName, actual);
6588
}
6689

67-
[Theory(DisplayName = "Parse() passed <TAG>")]
90+
[Theory(DisplayName = "Parse() parses <TAG>")]
6891
[MemberData(nameof(BodyHtmlElements))]
6992
public void Test003(string elementName)
7093
{
@@ -75,7 +98,7 @@ public void Test003(string elementName)
7598
.NodeName.ShouldBe(elementName, StringCompareShould.IgnoreCase);
7699
}
77100

78-
[Theory(DisplayName = "Parse() passed <TAG/>")]
101+
[Theory(DisplayName = "Parse() parses <TAG/>")]
79102
[MemberData(nameof(BodyHtmlElements))]
80103
public void Test004(string elementName)
81104
{
@@ -86,28 +109,43 @@ public void Test004(string elementName)
86109
.NodeName.ShouldBe(elementName, StringCompareShould.IgnoreCase);
87110
}
88111

89-
[Theory(DisplayName = "Parse() passed <TAG ...")]
112+
[Theory(DisplayName = "Parse() parses <TAG ...")]
90113
[MemberData(nameof(BodyHtmlElements))]
91114
public void Test005(string elementName)
92115
{
93116
var input = $@" <{elementName} foo bar {'\t'}{Environment.NewLine}";
94117
var actual = Parser.Parse(input).ToList();
95118

96119
actual.ShouldHaveSingleItem()
120+
.ShouldBeAssignableTo<IText>()
97121
.TextContent.ShouldBe(" ");
98122
}
99123

100-
[Theory(DisplayName = "Parse() passed <TAG .../")]
124+
[Theory(DisplayName = "Parse() parses <TAG .../")]
101125
[MemberData(nameof(BodyHtmlElements))]
102126
public void Test006(string elementName)
103127
{
104128
var input = $@" <{elementName}/ ";
105129
var actual = Parser.Parse(input).ToList();
106130

107131
actual.ShouldHaveSingleItem()
132+
.ShouldBeAssignableTo<IText>()
108133
.TextContent.ShouldBe(" ");
109134
}
110135

136+
[Theory(DisplayName = "Parse() parses SVG element <TAG>")]
137+
[MemberData(nameof(SvgElements))]
138+
public void Test020(string elementName)
139+
{
140+
var actual = Parser.Parse($@"<{elementName}>").ToList();
141+
142+
// SVG elements that are parsed outside of a SVG parent element will
143+
// be returned as an IHtmlUnknownElement, which is precise enough for our need.
144+
actual.ShouldHaveSingleItem()
145+
.ShouldBeAssignableTo<IHtmlUnknownElement>()
146+
.NodeName.ShouldBe(elementName, StringCompareShould.IgnoreCase);
147+
}
148+
111149
private static void VerifyElementParsedWithId(string expectedElementName, List<INode> actual)
112150
{
113151
var elm = actual.OfType<IElement>()

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "1.15",
3+
"version": "1.16",
44
"assemblyVersion": {
55
"precision": "revision"
66
},

0 commit comments

Comments
 (0)