Files

188 lines
6.9 KiB
C#

using Elternbeirat.Web.Shared;
namespace Elternbeirat.Web.Tests;
/// <summary>
/// Unit tests for the Markdown pipeline behind every editor-written body. Pure logic,
/// no PocketBase needed.
/// </summary>
public class MarkdownTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
public void Empty_input_renders_nothing(string? markdown) =>
Markdown.ToHtml(markdown).ShouldBe("");
[Fact]
public void Custom_container_renders_a_div_with_its_name_as_class()
{
var html = Markdown.ToHtml("::: kennzahlen\n- **seit 1975** Gründung\n:::");
html.ShouldContain("<div class=\"kennzahlen\">");
html.ShouldContain("<strong>seit 1975</strong>");
}
[Fact]
public void Unknown_container_renders_its_content_as_normal_text()
{
// A typo in the block name must cost the styling, never the text.
var html = Markdown.ToHtml("::: gibtsnicht\nNormaler Text.\n:::");
html.ShouldContain("<p>Normaler Text.</p>");
html.ShouldNotContain(":::");
}
[Fact]
public void Raw_html_is_escaped_not_passed_through()
{
// The body is rendered as a MarkupString; a passed-through <script> would run
// in the visitor's browser.
var html = Markdown.ToHtml("Hallo <script>alert(1)</script>\n\n<div onclick=\"x()\">Box</div>");
html.ShouldNotContain("<script>");
html.ShouldNotContain("<div onclick");
html.ShouldContain("&lt;script&gt;");
}
[Fact]
public void Pipe_table_renders_a_table()
{
var html = Markdown.ToHtml("| Fach | Raum |\n|---|---|\n| Mathe | A1 |");
html.ShouldContain("<table>");
html.ShouldContain("<td>Mathe</td>");
}
[Theory]
[InlineData("[kontakt@example.org](mailto:kontakt@example.org)")]
[InlineData("<mailto:kontakt@example.org>")] // autolink
[InlineData("[Satzung](/dokumente/satzung.pdf) ")] // trailing whitespace ignored
public void Link_alone_in_its_paragraph_is_marked(string markdown) =>
Markdown.ToHtml(markdown).ShouldStartWith($"<p class=\"{Markdown.LoneLinkClass}\">");
[Theory]
[InlineData("Schreiben Sie an [uns](mailto:kontakt@example.org).")]
[InlineData("[eins](/a) und [zwei](/b)")]
[InlineData("![Foto](/bild.jpg)")] // an image is not a link
[InlineData("Nur Text.")]
public void Paragraph_that_is_not_a_single_link_is_not_marked(string markdown) =>
Markdown.ToHtml(markdown).ShouldNotContain(Markdown.LoneLinkClass);
[Fact]
public void Lone_link_in_a_tight_list_marks_the_list_item()
{
// A tight list renders no <p>, so the class has to sit on the <li>.
var html = Markdown.ToHtml("- [Antrag](/dokumente/antrag.pdf)\n- Text mit [Link](/x)");
html.ShouldContain($"<li class=\"{Markdown.LoneLinkClass}\"><a href=\"/dokumente/antrag.pdf\">");
html.ShouldContain("<li>Text mit");
}
[Fact]
public void First_sentence_skips_the_heading_and_drops_the_markdown()
{
var sentence = Markdown.FirstSentence(
"# Vorstand\n\nWir sind **sieben** [Eltern](/board). Der Rest folgt.");
sentence.ShouldBe("Wir sind sieben Eltern.");
}
[Theory]
[InlineData("Wir helfen z. B. bei Anträgen. Mehr dazu unten.", "Wir helfen z. B. bei Anträgen.")]
[InlineData("Gewählt am 13. November 2025. Danach mehr.", "Gewählt am 13. November 2025.")]
[InlineData("Haben Sie Fragen? Schreiben Sie uns.", "Haben Sie Fragen?")]
[InlineData("Ohne Satzzeichen am Ende", "Ohne Satzzeichen am Ende")]
public void First_sentence_ends_at_a_real_sentence_end(string markdown, string expected) =>
Markdown.FirstSentence(markdown).ShouldBe(expected);
[Fact]
public void First_sentence_joins_soft_line_breaks_with_a_space()
{
// Editors wrap long lines; the wrap must not glue two words together.
Markdown.FirstSentence("Erste Zeile\nzweite Zeile.").ShouldBe("Erste Zeile zweite Zeile.");
}
[Fact]
public void First_sentence_drops_a_colon_that_leads_into_a_list() =>
Markdown.FirstSentence("Formulare zum Herunterladen:\n\n- Antrag")
.ShouldBe("Formulare zum Herunterladen");
[Fact]
public void First_sentence_is_never_cut_short()
{
// Long teasers are clamped by the tile's CSS, not by a character count.
var sentence = string.Join(' ', Enumerable.Repeat("Wort", 60)) + ".";
Markdown.FirstSentence(sentence).ShouldBe(sentence);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("# Nur eine Überschrift")]
[InlineData("- nur\n- eine Liste")]
[InlineData("::: hinweis\nText im Baustein.\n:::")]
public void First_sentence_is_empty_without_a_top_level_paragraph(string? markdown) =>
Markdown.FirstSentence(markdown).ShouldBe("");
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("## Nur eine Überschrift")]
public void Teaser_is_empty_without_paragraph_text(string? markdown) =>
Markdown.Teaser(markdown).ShouldBe("");
[Fact]
public void Teaser_keeps_a_short_body_whole() =>
Markdown.Teaser("Kurzer Text.").ShouldBe("Kurzer Text.");
[Fact]
public void Teaser_removes_markdown_and_skips_headings()
{
// Emphasis and link markup go, link text stays; the heading would repeat the
// card title, so it is left out. Paragraphs and list items are joined.
var teaser = Markdown.Teaser(
"## Rückblick\n\nDer **Basar** war ein [Erfolg](/posts).\n\n- Danke an alle\n- Bis bald");
teaser.ShouldBe("Der Basar war ein Erfolg. Danke an alle Bis bald");
}
[Fact]
public void Teaser_cuts_a_long_body_at_a_word_boundary()
{
var teaser = Markdown.Teaser("Der Basar war ein voller Erfolg.", 20);
// 20 characters end inside "voller"; the cut falls back to the space before it.
teaser.ShouldBe("Der Basar war ein…");
}
[Fact]
public void Teaser_keeps_a_word_that_ends_exactly_at_the_limit() =>
Markdown.Teaser("Der Basar war ein voller Erfolg.", 17).ShouldBe("Der Basar war ein…");
[Fact]
public void Teaser_drops_punctuation_left_at_the_cut() =>
Markdown.Teaser("Liebe Eltern, der Basar war schön.", 14).ShouldBe("Liebe Eltern…");
[Fact]
public void Teaser_cuts_a_single_overlong_word_hard() =>
Markdown.Teaser("Donaudampfschifffahrtsgesellschaft", 10).ShouldBe("Donaudampf…");
[Fact]
public void Teaser_defaults_to_160_characters_plus_ellipsis()
{
var body = string.Join(' ', Enumerable.Repeat("Wort", 100)); // 499 characters
var teaser = Markdown.Teaser(body);
teaser.Length.ShouldBeLessThanOrEqualTo(Markdown.TeaserLength + 1);
teaser.ShouldEndWith("Wort…");
}
[Fact]
public void Teaser_rejects_a_length_of_zero() =>
Should.Throw<ArgumentOutOfRangeException>(() => Markdown.Teaser("Text", 0));
}