Skip to content

Commit 78ec07b

Browse files
committed
Updated release notes to support MD changes on GitHub
1 parent 56cc8e7 commit 78ec07b

10 files changed

Lines changed: 744 additions & 14 deletions

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
// // Copyright (c) Microsoft Corporation.
2+
// // Licensed under the MIT License.
3+
4+
using EventLogExpert.UI.Services;
5+
6+
namespace EventLogExpert.UI.Tests.Services;
7+
8+
public sealed class ReleaseNotesMarkdownRendererTests
9+
{
10+
[Fact]
11+
public void RenderToHtml_BlankLineBetweenBullets_StartsNewList()
12+
{
13+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("- a\n\n- b");
14+
15+
Assert.Equal("<ul><li>a</li></ul><ul><li>b</li></ul>", html);
16+
}
17+
18+
[Fact]
19+
public void RenderToHtml_BlankLineSeparatesParagraphs()
20+
{
21+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("para one\n\npara two");
22+
23+
Assert.Equal("<p>para one</p><p>para two</p>", html);
24+
}
25+
26+
[Fact]
27+
public void RenderToHtml_Bold_RendersStrong()
28+
{
29+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**bold text**");
30+
31+
Assert.Contains("<strong>bold text</strong>", html);
32+
}
33+
34+
[Fact]
35+
public void RenderToHtml_BoldDoesNotInterfereWithItalic()
36+
{
37+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**bold** and *italic*");
38+
39+
Assert.Contains("<strong>bold</strong>", html);
40+
Assert.Contains("<em>italic</em>", html);
41+
}
42+
43+
[Theory]
44+
[InlineData("- item one")]
45+
[InlineData("* item one")]
46+
[InlineData("+ item one")]
47+
public void RenderToHtml_BulletPrefixes_RenderAsList(string markdown)
48+
{
49+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml(markdown);
50+
51+
Assert.Equal("<ul><li>item one</li></ul>", html);
52+
}
53+
54+
[Fact]
55+
public void RenderToHtml_CodePlaceholderSentinelInInput_DoesNotConfuseParser()
56+
{
57+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("text with \u00010\u0001 sentinel and `actual code`");
58+
59+
Assert.Contains("<code>actual code</code>", html);
60+
Assert.False(html.Contains('\u0001'), "rendered HTML must not contain the internal placeholder sentinel");
61+
}
62+
63+
[Fact]
64+
public void RenderToHtml_CodeSpanContents_NotProcessedAsMarkdown()
65+
{
66+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("`**not bold**`");
67+
68+
Assert.Contains("<code>**not bold**</code>", html);
69+
Assert.DoesNotContain("<strong>", html);
70+
}
71+
72+
[Fact]
73+
public void RenderToHtml_CrlfLineEndings_HandledLikeLf()
74+
{
75+
var lf = ReleaseNotesMarkdownRenderer.RenderToHtml("# Heading\n\n- item");
76+
var crlf = ReleaseNotesMarkdownRenderer.RenderToHtml("# Heading\r\n\r\n- item");
77+
78+
Assert.Equal(lf, crlf);
79+
}
80+
81+
[Fact]
82+
public void RenderToHtml_HashMidLine_NotTreatedAsHeading()
83+
{
84+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("Some text ## not a heading");
85+
86+
Assert.DoesNotContain("<h2>", html);
87+
}
88+
89+
[Fact]
90+
public void RenderToHtml_HashWithoutSpace_NotTreatedAsHeading()
91+
{
92+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("#NotAHeading");
93+
94+
Assert.DoesNotContain("<h1>", html);
95+
Assert.Contains("#NotAHeading", html);
96+
}
97+
98+
[Theory]
99+
[InlineData("# Heading", "<h1>Heading</h1>")]
100+
[InlineData("## Heading", "<h2>Heading</h2>")]
101+
[InlineData("### Heading", "<h3>Heading</h3>")]
102+
[InlineData("#### Heading", "<h4>Heading</h4>")]
103+
public void RenderToHtml_Headings(string markdown, string expected)
104+
{
105+
Assert.Equal(expected, ReleaseNotesMarkdownRenderer.RenderToHtml(markdown));
106+
}
107+
108+
[Fact]
109+
public void RenderToHtml_HttpLink_RendersAnchor()
110+
{
111+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("[link](http://example.com)");
112+
113+
Assert.Contains("<a href=\"http://example.com\"", html);
114+
}
115+
116+
[Fact]
117+
public void RenderToHtml_HttpsLink_RendersAnchor()
118+
{
119+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("see [docs](https://example.com/page)");
120+
121+
Assert.Contains("<a href=\"https://example.com/page\" target=\"_blank\" rel=\"noopener noreferrer\">docs</a>", html);
122+
}
123+
124+
[Fact]
125+
public void RenderToHtml_InlineCode_RendersCodeTag()
126+
{
127+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("use `Foo()` to call");
128+
129+
Assert.Contains("<code>Foo()</code>", html);
130+
}
131+
132+
[Fact]
133+
public void RenderToHtml_Italic_RendersEm()
134+
{
135+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("paragraph with *italic* word");
136+
137+
Assert.Contains("<em>italic</em>", html);
138+
}
139+
140+
[Fact]
141+
public void RenderToHtml_LegacyBulletList_RendersCleanly()
142+
{
143+
const string markdown = "## Changes:\n\n- Fixed LF issue in App.xaml\n- Updated Azure yml to .NET 8";
144+
145+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml(markdown);
146+
147+
Assert.Contains("<h2>Changes:</h2>", html);
148+
Assert.Contains("<ul><li>Fixed LF issue in App.xaml</li><li>Updated Azure yml to .NET 8</li></ul>", html);
149+
}
150+
151+
[Fact]
152+
public void RenderToHtml_MultipleBullets_GroupedInOneList()
153+
{
154+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("- a\n- b\n- c");
155+
156+
Assert.Equal("<ul><li>a</li><li>b</li><li>c</li></ul>", html);
157+
}
158+
159+
[Fact]
160+
public void RenderToHtml_MultipleCodeSpansOnOneLine_AllRendered()
161+
{
162+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("call `Foo()` then `Bar()` then `Baz()`");
163+
164+
Assert.Contains("<code>Foo()</code>", html);
165+
Assert.Contains("<code>Bar()</code>", html);
166+
Assert.Contains("<code>Baz()</code>", html);
167+
}
168+
169+
[Fact]
170+
public void RenderToHtml_NullOrWhitespace_ReturnsEmpty()
171+
{
172+
Assert.Equal(string.Empty, ReleaseNotesMarkdownRenderer.RenderToHtml(null!));
173+
Assert.Equal(string.Empty, ReleaseNotesMarkdownRenderer.RenderToHtml(string.Empty));
174+
Assert.Equal(string.Empty, ReleaseNotesMarkdownRenderer.RenderToHtml(" \n\n "));
175+
}
176+
177+
[Fact]
178+
public void RenderToHtml_PlainTextLines_RenderAsParagraph()
179+
{
180+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("first line\nsecond line");
181+
182+
Assert.Equal("<p>first line<br />second line</p>", html);
183+
}
184+
185+
[Theory]
186+
[InlineData("[xss](https://example.com\"onload=alert(1))")]
187+
[InlineData("[xss](https://example.com'onclick='alert(1))")]
188+
public void RenderToHtml_QuotesInUrl_AreEscapedNotInjected(string markdown)
189+
{
190+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml(markdown);
191+
192+
Assert.DoesNotContain("\" onload=", html);
193+
Assert.DoesNotContain("\"onload=", html);
194+
Assert.DoesNotContain("\" onclick=", html);
195+
Assert.DoesNotContain("'onclick=", html);
196+
}
197+
198+
[Fact]
199+
public void RenderToHtml_RawHtmlInInput_IsEscapedNotRendered()
200+
{
201+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("<img src=x onerror=alert(1)>");
202+
203+
Assert.DoesNotContain("<img", html);
204+
Assert.Contains("&lt;img", html);
205+
}
206+
207+
[Fact]
208+
public void RenderToHtml_RichLayoutSample_RendersAllElements()
209+
{
210+
const string markdown = """
211+
## What's New in v1.2.3
212+
213+
### Features
214+
- **Column reordering** with persistent sizing
215+
- Support for [exported logs](https://example.com/docs)
216+
217+
### Bug Fixes
218+
- Fixed crash when opening empty file
219+
""";
220+
221+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml(markdown);
222+
223+
Assert.Contains("<h2>What&#39;s New in v1.2.3</h2>", html);
224+
Assert.Contains("<h3>Features</h3>", html);
225+
Assert.Contains("<h3>Bug Fixes</h3>", html);
226+
Assert.Contains("<strong>Column reordering</strong>", html);
227+
Assert.Contains("<a href=\"https://example.com/docs\"", html);
228+
Assert.Contains("<li>Fixed crash when opening empty file</li>", html);
229+
}
230+
231+
[Fact]
232+
public void RenderToHtml_ScriptTagInInput_IsEscaped()
233+
{
234+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("<script>alert(1)</script>");
235+
236+
Assert.DoesNotContain("<script>", html);
237+
Assert.Contains("&lt;script&gt;", html);
238+
}
239+
240+
[Fact]
241+
public void RenderToHtml_TitleIsHtmlEscaped()
242+
{
243+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("<script>alert(1)</script>", string.Empty);
244+
245+
Assert.DoesNotContain("<script>", html);
246+
Assert.Contains("&lt;script&gt;", html);
247+
}
248+
249+
[Fact]
250+
public void RenderToHtml_TitleOnly_RendersH1()
251+
{
252+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("Release notes for v1.0", string.Empty);
253+
254+
Assert.Equal("<h1>Release notes for v1.0</h1>", html);
255+
}
256+
257+
[Fact]
258+
public void RenderToHtml_UnmatchedBoldMarkers_LeftAsLiteral()
259+
{
260+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**unclosed bold");
261+
262+
Assert.DoesNotContain("<strong>", html);
263+
Assert.Contains("**unclosed bold", html);
264+
}
265+
266+
[Theory]
267+
[InlineData("[evil](javascript:alert(1))")]
268+
[InlineData("[evil](data:text/html,<script>)")]
269+
[InlineData("[evil](file:///etc/passwd)")]
270+
[InlineData("[evil](ftp://example.com)")]
271+
public void RenderToHtml_UnsafeLinkSchemes_NotRenderedAsAnchor(string markdown)
272+
{
273+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml(markdown);
274+
275+
Assert.DoesNotContain("<a ", html);
276+
Assert.DoesNotContain("href=", html);
277+
}
278+
279+
[Fact]
280+
public void RenderToHtml_UrlWithAmpersand_PreservesEscapedEntity()
281+
{
282+
var html = ReleaseNotesMarkdownRenderer.RenderToHtml("[link](https://example.com/path?a=1&b=2)");
283+
284+
Assert.Contains("href=\"https://example.com/path?a=1&amp;b=2\"", html);
285+
}
286+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// // Copyright (c) Microsoft Corporation.
2+
// // Licensed under the MIT License.
3+
4+
using EventLogExpert.UI.Services;
5+
using EventLogExpert.UI.Tests.TestUtils.Constants;
6+
7+
namespace EventLogExpert.UI.Tests.Services;
8+
9+
public sealed class ReleaseNotesNormalizerTests
10+
{
11+
[Fact]
12+
public void Normalize_BulletWithoutCommitPrefix_ConvertsToDashStyle()
13+
{
14+
const string raw = "* Just a description with no commit id";
15+
16+
var result = ReleaseNotesNormalizer.Normalize(raw);
17+
18+
Assert.Equal("- Just a description with no commit id", result);
19+
}
20+
21+
[Fact]
22+
public void Normalize_LegacyFixture_ProducesCleanBullets()
23+
{
24+
var result = ReleaseNotesNormalizer.Normalize(Constants.GitHubReleaseNotes);
25+
26+
Assert.Contains("## Changes:", result);
27+
Assert.Contains("- Fixed LF issue in App.xaml and added custom width for ultrawide monitors", result);
28+
Assert.Contains("- Updated Azure yml to .NET 8", result);
29+
Assert.DoesNotContain("f7f7aff67132dc32c92519a1bc250e1a81606e2b", result);
30+
Assert.DoesNotContain("66b7d6883807a5c518ffcd59f92e07e528a5636a", result);
31+
}
32+
33+
[Fact]
34+
public void Normalize_LegacyShaBullet_StripsCommitId()
35+
{
36+
const string raw = "* f7f7aff67132dc32c92519a1bc250e1a81606e2b Fixed LF issue in App.xaml";
37+
38+
var result = ReleaseNotesNormalizer.Normalize(raw);
39+
40+
Assert.Equal("- Fixed LF issue in App.xaml", result);
41+
}
42+
43+
[Fact]
44+
public void Normalize_MarkdownLinkBullet_StripsLinkPrefix()
45+
{
46+
const string raw = "* [56cc8e7](https://github.com/microsoft/EventLogExpert/commit/56cc8e79f381b38d0ae32467265e3ca77dd16d74) Fixed CI issue";
47+
48+
var result = ReleaseNotesNormalizer.Normalize(raw);
49+
50+
Assert.Equal("- Fixed CI issue", result);
51+
}
52+
53+
[Fact]
54+
public void Normalize_NullOrWhitespace_ReturnsEmpty()
55+
{
56+
Assert.Equal(string.Empty, ReleaseNotesNormalizer.Normalize(null));
57+
Assert.Equal(string.Empty, ReleaseNotesNormalizer.Normalize(string.Empty));
58+
Assert.Equal(string.Empty, ReleaseNotesNormalizer.Normalize(" \r\n "));
59+
}
60+
61+
[Fact]
62+
public void Normalize_RichAuthoredMarkdown_PassesThroughUnchanged()
63+
{
64+
const string raw = """
65+
## What's New in v1.2.3
66+
67+
### Features
68+
- **Column reordering** with persistent sizing
69+
- Support for [exported logs](https://example.com/docs)
70+
71+
### Bug Fixes
72+
- Fixed crash when opening empty file
73+
""";
74+
75+
var result = ReleaseNotesNormalizer.Normalize(raw);
76+
77+
Assert.Contains("## What's New in v1.2.3", result);
78+
Assert.Contains("### Features", result);
79+
Assert.Contains("- **Column reordering** with persistent sizing", result);
80+
Assert.Contains("[exported logs](https://example.com/docs)", result);
81+
}
82+
}

0 commit comments

Comments
 (0)