Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ConsoleMarkdownRenderer.Tests/DisplayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,12 @@ public async Task DisplayTests_NonInteractiveTerminalWithNoLinksExitsCleanlyAsyn
// Use a non-interactive displayer with a markdown file that has no links
using var nonInteractiveDisplayer = CreateNonInteractiveDisplayer();

var text = "# Just a heading\n\nNo links here.";
var text = "## Just a heading\n\nNo links here.";
await nonInteractiveDisplayer.DisplayMarkdownAsync(text);

// Should not show warning since there are no links to display
AssertCrossPlatStringMatch(@"
# Just a heading #
## Just a heading ##

No links here.

Expand Down
133 changes: 133 additions & 0 deletions ConsoleMarkdownRenderer.Tests/FigletTextStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using BoxOfYellow.ConsoleMarkdownRenderer.Styling;

namespace BoxOfYellow.ConsoleMarkdownRenderer.Tests
{
/// <summary>
/// Tests for <see cref="FigletTextStyle"/> and the shared <see cref="IHeaderStyle"/> contract.
/// </summary>
[TestClass]
public class FigletTextStyleTests
{
[TestMethod]
public void FigletTextStyle_DefaultsAreNull()
{
var style = new FigletTextStyle();
Assert.IsNull(style.Justification);
Assert.IsNull(style.Foreground);
}

[TestMethod]
public void FigletTextStyle_PropertiesArePreserved()
{
var style = new FigletTextStyle(
justification: TextJustification.Center,
foreground: TextColor.Red);

Assert.AreEqual(TextJustification.Center, style.Justification);
Assert.AreEqual(TextColor.Red, style.Foreground);
}

[TestMethod]
public void FigletTextStyle_Equality_SameValues()
{
var a = new FigletTextStyle(justification: TextJustification.Right, foreground: TextColor.Green);
var b = new FigletTextStyle(justification: TextJustification.Right, foreground: TextColor.Green);

Assert.AreEqual(a, b);
Assert.AreEqual(a.GetHashCode(), b.GetHashCode());
}

[TestMethod]
public void FigletTextStyle_Equality_DifferentJustification()
{
var a = new FigletTextStyle(justification: TextJustification.Left);
var b = new FigletTextStyle(justification: TextJustification.Right);

Assert.AreNotEqual(a, b);
}

[TestMethod]
public void FigletTextStyle_Equality_DifferentForeground()
{
var a = new FigletTextStyle(foreground: TextColor.Red);
var b = new FigletTextStyle(foreground: TextColor.Blue);

Assert.AreNotEqual(a, b);
}

[TestMethod]
public void FigletTextStyle_ImplementsIHeaderStyle()
{
// Both FigletTextStyle and the existing TextStyle satisfy the shared IHeaderStyle
// contract so they can be assigned to DisplayOptions.Header / Headers interchangeably.
IHeaderStyle figlet = new FigletTextStyle(justification: TextJustification.Center);
IHeaderStyle plain = new TextStyle(decoration: TextDecoration.Bold);

Assert.AreEqual(TextJustification.Center, figlet.Justification);
Assert.IsNull(plain.Justification);
}

[TestMethod]
public void TextStyle_Justification_IsNullViaIHeaderStyle()
{
// The Justification property on TextStyle is provided exclusively through the
// IHeaderStyle interface (explicit implementation) and must always return null.
IHeaderStyle plain = new TextStyle(decoration: TextDecoration.Bold, foreground: TextColor.Red);
Assert.IsNull(plain.Justification);
}

[TestMethod]
public void TextStyle_FontPath_IsNullViaIHeaderStyle()
{
// FontPath is explicitly implemented on TextStyle and must always return null --
// custom FIGlet fonts are only meaningful for FigletTextStyle.
IHeaderStyle plain = new TextStyle();
Assert.IsNull(plain.FontPath);
}

[TestMethod]
public void TextStyle_ExposesForegroundBackgroundDecorationViaIHeaderStyle()
{
// Foreground/Background/Decoration on TextStyle are implicit interface members --
// they round-trip through the IHeaderStyle interface unchanged.
IHeaderStyle plain = new TextStyle(
decoration: TextDecoration.Italic,
foreground: TextColor.Red,
background: TextColor.Blue);

Assert.AreEqual(TextDecoration.Italic, plain.Decoration);
Assert.AreEqual(TextColor.Red, plain.Foreground);
Assert.AreEqual(TextColor.Blue, plain.Background);
}

[TestMethod]
public void FigletTextStyle_BackgroundAndDecoration_HardCodedViaIHeaderStyle()
{
// FigletText does not support a background color or text decoration so those
// members on the IHeaderStyle interface are hard-coded.
IHeaderStyle figlet = new FigletTextStyle(foreground: TextColor.Green);

Assert.AreEqual(TextColor.Green, figlet.Foreground);
Assert.IsNull(figlet.Background);
Assert.AreEqual(TextDecoration.None, figlet.Decoration);
}

[TestMethod]
public void FigletTextStyle_FontPath_Preserved()
{
var style = new FigletTextStyle(fontPath: "/some/path/font.flf");

Assert.AreEqual("/some/path/font.flf", style.FontPath);
Assert.AreEqual("/some/path/font.flf", ((IHeaderStyle)style).FontPath);
}

[TestMethod]
public void FigletTextStyle_Equality_DifferentFontPath()
{
var a = new FigletTextStyle(fontPath: "/a.flf");
var b = new FigletTextStyle(fontPath: "/b.flf");

Assert.AreNotEqual(a, b);
}
}
}
145 changes: 138 additions & 7 deletions ConsoleMarkdownRenderer.Tests/RendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,14 @@ public void RendererTests_MarkedTest(bool useCrazy)
[DataRow(false)]
[DataRow(true)]
public void RendererTests_HeaderTest(bool useCrazy)
// The default DisplayOptions configures H1 as a FigletTextStyle, so H1's literal
// text ("Level One") is replaced by FIGlet ASCII art and is not asserted here.
// H2 and H3 still fall through to the default Header style.
=> AssertMarkdownYieldsFormat(
"headingBlock",
text: useCrazy
? "Level One Level Two Level Three"
: "# Level One # ## Level Two ## ### Level Three ###",
? "Level Two with here Level Three with bold word"
: "## Level Two with code here ## ### Level Three with bold word ###",
new Style(decoration: Decoration.Bold | Decoration.Invert | Decoration.Underline),
useCrazy);

Expand All @@ -177,14 +180,17 @@ public void RendererTests_LevelSpecificHeaderTest()
{
WrapHeader = false,
};
options.Headers.Add("blue on green");
options.Headers.Add("green on blue");
// Clear the default Headers list (which configures H1 as a FigletTextStyle) so the
// for-loop below exercises the styled-markup path for the levels it specifies.
options.Headers.Clear();
options.Headers.Add((TextStyle)"blue on green");
options.Headers.Add((TextStyle)"green on blue");

string[] levels = ["One", "Two", "Three"];

for (int index = 0; index < levels.Length; index++)
{
TextStyle expected = index < options.Headers.Count
IHeaderStyle expected = index < options.Headers.Count
? options.Headers[index]
: options.Header;

Expand All @@ -193,12 +199,137 @@ public void RendererTests_LevelSpecificHeaderTest()
AssertMarkdownYieldsFormat(
"headingBlock",
text: $"Level {levels[index]}",
expected.ToSpectreStyle(),
((TextStyle)expected).ToSpectreStyle(),
useCrazy: false,
options);
}
}

[TestMethod]
public void RendererTests_FigletHeaderRendersAsciiArt()
{
// Configure H1 to use FigletTextStyle with both a Foreground color and inline
// emphasis + code content in the heading. This exercises four branches of the
// FIGlet path: the Foreground -> figlet.Color assignment, AppendInline recursion
// through a ContainerInline (the EmphasisInline produced by "*One*"), and the
// CodeInline branch (the "`code`" span). The literal heading text should
// therefore not appear in the output because each letter is split across
// multiple lines of glyph characters.
DisplayOptions options = new();
options.Headers[0] = new FigletTextStyle(
justification: TextJustification.Left,
foreground: TextColor.Red);

const string markdown = "# Level *One* `code`\n\nbody\n";
ConsoleUnderTest.Write(Renderer(markdown, options));

var output = ConsoleUnderTest.Output;

Assert.DoesNotContain("Level One", output,
$"FIGlet rendered heading should not contain literal text 'Level One':\n{output}");
Assert.DoesNotContain("code", output,
$"FIGlet rendered heading should not contain literal text 'code':\n{output}");
Assert.DoesNotContain("#", output,
$"FIGlet rendered heading should not include any '#' characters:\n{output}");
// FIGlet glyphs are made of underscores, pipes and slashes.
Assert.IsTrue(output.Contains('_') && output.Contains('|'),
$"Expected FIGlet glyph characters ('_' and '|') in output:\n{output}");
// The non-heading body content should still render normally.
Assert.Contains("body", output, $"Body text should still be rendered:\n{output}");
Comment thread
boxofyellow marked this conversation as resolved.
Outdated
}

[TestMethod]
public void RendererTests_FigletHeaderOnlyAppliesToConfiguredLevel()
{
// Default Headers[0] uses FigletTextStyle for H1; H2 and H3 fall through to the
// styled header style. Verify that deeper levels keep their "#"-wrapping intact.
DisplayOptions options = new();

ConsoleUnderTest.Write(Renderer(GetResourceContent("headingBlock", "md"), options));

var output = ConsoleUnderTest.Output;

// H1 should be rendered via FIGlet so its literal text should be absent.
Assert.DoesNotContain("Level One", output,
$"H1 should be FIGlet rendered:\n{output}");
// H2 and H3 should still be rendered as styled markup wrapped in '#'s.
Assert.Contains("## Level Two with code here ##", output,
$"H2 should remain styled with '##' wrapping:\n{output}");
Assert.Contains("### Level Three with bold word ###", output,
$"H3 should remain styled with '###' wrapping:\n{output}");
Comment thread
boxofyellow marked this conversation as resolved.
Outdated
}

[TestMethod]
public void RendererTests_FigletDefaultCanBeOverriddenWithTextStyle()
{
// Regression guard: existing callers can opt H1 out of the new FIGlet default by
// replacing Headers[0] with a plain TextStyle (or by clearing Headers entirely).
// When that is done the heading renderer must emit the styled, "#"-wrapped markup
// exactly as before.
DisplayOptions options = new();
Comment thread
boxofyellow marked this conversation as resolved.
options.Headers[0] = new TextStyle(decoration: TextDecoration.Bold);

ConsoleUnderTest.Write(Renderer(GetResourceContent("headingBlock", "md"), options));

const string expected = """
┌────────────────────────────────────┐
│ │
│ # Level One # │
│ │
│ │
│ ## Level Two with code here ## │
│ │
│ │
│ ### Level Three with bold word ### │
│ │
└────────────────────────────────────┘

""";

AssertCrossPlatStringMatch(expected, ConsoleUnderTest.Output);
}

[TestMethod]
public void RendererTests_FigletEmptyHeadingFallsBackToStyledMarkup()
{
// FigletText cannot render an empty string. When the heading has no text the
// renderer should fall through to the styled-markup path so the level marker
// (e.g. "# #" with WrapHeader=true) is still emitted.
DisplayOptions options = new();
// Sanity check: H1 is configured to use FIGlet by default.
Assert.IsInstanceOfType<FigletTextStyle>(options.EffectiveHeader(1));

ConsoleUnderTest.Write(Renderer("#\n", options));

var output = ConsoleUnderTest.Output;
Assert.Contains("#", output,
$"Empty H1 should fall back to styled '#'-wrapped markup:\n{output}");
}

[TestMethod]
public void RendererTests_FigletFontPathLoadsCustomFont()
{
// When FontPath is set the renderer should load the custom .flf font and use it
// to render the FIGlet text. Compare against a known-good expected output produced
// by the bundled shadow.flf font.
const string markdown = "# Hi\n";

var fontPath = Path.Combine(DataPath, "fonts", "shadow.flf");
Assert.IsTrue(File.Exists(fontPath), $"Test font file should exist at {fontPath}");

var expectedPath = Path.Combine(DataPath, "expected", "figletCustomFont.txt");
Assert.IsTrue(File.Exists(expectedPath), $"Expected output file should exist at {expectedPath}");
var expected = File.ReadAllText(expectedPath);

var customOptions = new DisplayOptions
{
Headers = new() { new FigletTextStyle(fontPath: fontPath) },
};
ConsoleUnderTest.Write(Renderer(markdown, customOptions));

AssertCrossPlatStringMatch(expected, ConsoleUnderTest.Output);
}

[TestMethod]
[DataRow("htmlBlock", "<table> <tr> <td>1</td> <td>2</td> </tr> <tr> <td>3</td> <td>4</td> </tr> </table>")]
[DataRow("htmlInline", "<span>html</span>")]
Expand Down Expand Up @@ -540,7 +671,7 @@ private static Dictionary<string, int> Counts(string text)
Footnote = c_crazyFormat,
FootnoteGroup = c_crazyFormat,
FootnoteLink = c_crazyFormat,
Header = c_crazyFormat,
Header = (TextStyle)c_crazyFormat,
HtmlBlock = c_crazyFormat,
HtmlInline = c_crazyFormat,
Inserted = c_crazyFormat,
Expand Down
43 changes: 43 additions & 0 deletions ConsoleMarkdownRenderer.Tests/TextJustificationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using BoxOfYellow.ConsoleMarkdownRenderer.Styling;
using Spectre.Console;

namespace BoxOfYellow.ConsoleMarkdownRenderer.Tests
{
/// <summary>
/// Tests for <see cref="TextJustification"/> to ensure it stays in sync with
/// Spectre.Console's <see cref="Justify"/> enum. The pattern mirrors
/// <see cref="TextDecorationTests"/>.
/// </summary>
[TestClass]
public class TextJustificationTests
{
/// <summary>
/// Verifies that every value of Spectre.Console's <see cref="Justify"/> enum has a
/// corresponding value in our <see cref="TextJustification"/> enum. If Spectre adds
/// new justifications, this test will fail to remind us to update.
/// </summary>
[TestMethod]
public void TextJustification_HasAllSpectreJustifyValues()
{
var spectreNames = Enum.GetNames(typeof(Justify)).ToList();
var ourNames = Enum.GetNames(typeof(TextJustification)).ToList();

var missing = spectreNames.Where(name => !ourNames.Contains(name)).ToList();

if (missing.Count > 0)
{
Assert.Fail(
$"TextJustification is missing the following values from Spectre.Console.Justify: {string.Join(", ", missing)}");
}
}

[TestMethod]
[DataRow(TextJustification.Left, Justify.Left)]
[DataRow(TextJustification.Right, Justify.Right)]
[DataRow(TextJustification.Center, Justify.Center)]
public void TextJustification_ConvertsToSpectreJustify(TextJustification source, Justify expected)
{
Assert.AreEqual(expected, source.ToSpectreJustify());
}
}
}
Loading
Loading