using Elternbeirat.Web.Shared; using Markdig.Syntax; namespace Elternbeirat.Web.Tests; /// /// Unit tests for the person cards of the "::: team" block: initials, avatar colour, /// reading the list-item convention and the rendered card. Pure logic, no PocketBase /// needed. /// public class TeamMemberTests { [Theory] [InlineData("Jenny Reger-Stilgenbauer", "JR")] // a double name is one word [InlineData("Özlem Ünal", "ÖÜ")] // umlauts kept, not folded to O/U [InlineData("ömer şahin", "ÖŞ")] // lower case is raised [InlineData("Isabell", "I")] // a single word gives one letter [InlineData("Anna Maria Müller", "AM")] // first and last word only [InlineData(" Maik Palm ", "MP")] // extra whitespace ignored [InlineData("", "")] [InlineData("123", "")] // no letter, no initials public void Initials_are_first_and_last_word(string name, string expected) => TeamMember.InitialsOf(name).ShouldBe(expected); [Fact] public void Color_is_stable_and_in_range() { // Pinned values: the colour must not change between restarts or releases, so a // change to the hash shows up here instead of recolouring the board. TeamMember.ColorOf("Maik Palm").ShouldBe(TeamMember.ColorOf(" Maik Palm ")); TeamMember.ColorOf("Maik Palm").ShouldBe(4); TeamMember.ColorOf("Gentiana Erol").ShouldBe(5); new[] { "Maik Palm", "Gentiana Erol", "Raul Chira", "Marion Leininger", "Isabell Vesper", "" } .ShouldAllBe(name => TeamMember.ColorOf(name) >= 1 && TeamMember.ColorOf(name) <= TeamMember.ColorCount); } [Fact] public void Item_with_role_duties_and_note_is_read_in_full() { var (member, note) = Read("- **Maik Palm** (Vorsitzender) \n *Schulkonferenz, Mensarat , Homepage* \n Mitglied im Schulbeirat."); member.Name.ShouldBe("Maik Palm"); member.Role.ShouldBe("Vorsitzender"); member.Duties.ShouldBe(["Schulkonferenz", "Mensarat", "Homepage"]); Markdown.PlainText(note).ShouldBe("Mitglied im Schulbeirat."); } [Fact] public void Item_without_role_still_reads_the_duties() { var (member, note) = Read("- **Jenny Reger-Stilgenbauer** \n *Schulkonferenz, Schaukasten*"); member.Role.ShouldBeNull(); member.Duties.ShouldBe(["Schulkonferenz", "Schaukasten"]); note.ShouldBeEmpty(); } [Fact] public void Item_with_only_a_name_has_no_role_and_no_duties() { var (member, note) = Read("- **Isabell Vesper**"); member.Name.ShouldBe("Isabell Vesper"); member.Role.ShouldBeNull(); member.Duties.ShouldBeEmpty(); note.ShouldBeEmpty(); } [Fact] public void Soft_line_breaks_count_like_hard_ones() { // Editors forget the two trailing spaces; the card must not depend on them. var (member, _) = Read("- **Raul Chira** (2. Stellvertreter)\n *Mensarat*"); member.Role.ShouldBe("2. Stellvertreter"); member.Duties.ShouldBe(["Mensarat"]); } [Fact] public void Second_line_that_is_not_only_italic_becomes_the_note() { var (member, note) = Read("- **Rebecca Goldmann** \n Neu im Team, *herzlich willkommen*."); member.Duties.ShouldBeEmpty(); Markdown.PlainText(note).ShouldBe("Neu im Team, herzlich willkommen."); } [Theory] [InlineData("- Michael Weber, Beisitz")] // no bold at all [InlineData("- Beisitz: **Michael Weber**")] // bold, but not at the start [InlineData("- *Michael Weber*")] // italic, not bold public void Item_without_convention_is_not_read(string markdown) => TeamMember.Read(FirstParagraph(markdown)).ShouldBeNull(); [Fact] public void Team_block_renders_person_cards() { var html = Markdown.ToHtml( "::: team\n- **Maik Palm** (Vorsitzender) \n *Schulkonferenz, Mensarat* \n Siehe [Kontakt](/contact).\n:::"); html.ShouldContain("
"); html.ShouldContain($"
  • "); html.ShouldContain("MP"); html.ShouldContain("Maik Palm"); html.ShouldContain("Vorsitzender"); html.ShouldContain("Schulkonferenz"); html.ShouldContain("Mensarat"); html.ShouldContain("Siehe Kontakt."); // links survive html.ShouldNotContain("(Vorsitzender)"); } [Fact] public void Item_without_convention_renders_as_plain_card_with_its_text() { var html = Markdown.ToHtml("::: team\n- Michael Weber, Beisitz\n:::"); html.ShouldContain($"
  • Michael Weber, Beisitz
  • "); html.ShouldNotContain("team-avatar"); } [Fact] public void Editor_text_in_a_card_is_escaped() { var html = Markdown.ToHtml("::: team\n- **Name** (Rolle \"x\")\n:::"); html.ShouldNotContain(""); html.ShouldContain("<b>Name</b>"); } [Theory] [InlineData("::: team gross")] [InlineData("::: team groß")] [InlineData("::: team GROSS")] public void Large_modifier_adds_its_class(string opening) => Markdown.ToHtml($"{opening}\n- **Maik Palm**\n:::").ShouldContain("
    "); [Fact] public void Mistyped_modifier_keeps_the_grid() { // A typo in the modifier costs the larger size, not the cards. var html = Markdown.ToHtml("::: team grss\n- **Maik Palm**\n:::"); html.ShouldContain("
    "); html.ShouldContain("team-card"); } [Fact] public void Lists_outside_a_team_block_are_untouched() { var html = Markdown.ToHtml("- **Maik Palm** (Vorsitzender)"); html.ShouldBe("
      \n
    • Maik Palm (Vorsitzender)
    • \n
    \n"); } private static (TeamMember Member, Markdig.Syntax.Inlines.Inline[] Note) Read(string markdown) => TeamMember.Read(FirstParagraph(markdown)) ?? throw new ShouldAssertException("The item was not read as a person."); private static ParagraphBlock FirstParagraph(string markdown) => Markdown.Parse(markdown).Descendants().First(); }