Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
154 changes: 154 additions & 0 deletions ConsoleMarkdownRenderer.Tests/FigletTextStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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 : TestWithFileCleanupBase
{
[TestMethod]
public void FigletTextStyle_DefaultsAreNull()
{
var style = FigletTextStyle.Create();
Assert.IsNull(style.Justification);
Assert.IsNull(style.Foreground);
Assert.IsNull(style.FontPath);
Assert.IsNull(style.Font);
}

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

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

[TestMethod]
public void FigletTextStyle_Create_PreservesProperties()
{
var created = FigletTextStyle.Create(
justification: TextJustification.Right,
foreground: TextColor.Green);

Assert.AreEqual(TextJustification.Right, created.Justification);
Assert.AreEqual(TextColor.Green, created.Foreground);
Assert.IsNull(created.FontPath);
Assert.IsNull(created.Font);
Assert.AreEqual(FigletTextStyle.Create(TextJustification.Right, TextColor.Green), created);
}

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

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

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

Assert.AreNotEqual(a, b);
}

[TestMethod]
public void FigletTextStyle_Equality_DifferentForeground()
{
var a = FigletTextStyle.Create(foreground: TextColor.Red);
var b = FigletTextStyle.Create(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 = FigletTextStyle.Create(foreground: TextColor.Green);
IHeaderStyle plain = new TextStyle(decoration: TextDecoration.Bold);

Assert.AreEqual(TextColor.Green, figlet.Foreground);
Assert.AreEqual(TextDecoration.Bold, plain.Decoration);
}

[TestMethod]
public void TextStyle_ExposesForegroundBackgroundDecorationViaIHeaderStyle()
{
// Foreground/Background/Decoration on TextStyle 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 = FigletTextStyle.Create(foreground: TextColor.Green);

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

private static string BundledFontPath
=> Path.Combine(AppContext.BaseDirectory, "data", "fonts", "shadow.flf");

[TestMethod]
public async Task FigletTextStyle_CreateAsync_LoadsFont()
{
var style = await FigletTextStyle.CreateAsync(
BundledFontPath,
justification: TextJustification.Center,
foreground: TextColor.Green);

Assert.AreEqual(BundledFontPath, style.FontPath);
Assert.AreEqual(TextJustification.Center, style.Justification);
Assert.AreEqual(TextColor.Green, style.Foreground);
Assert.IsNotNull(style.Font);
}

[TestMethod]
public async Task FigletTextStyle_CreateAsync_InvalidPath_Throws()
{
// Loading happens at CreateAsync time so an invalid path surfaces there
// (rather than later at render time).
await Assert.ThrowsExactlyAsync<FileNotFoundException>(
() => FigletTextStyle.CreateAsync(
Path.Combine(AppContext.BaseDirectory, "data", "fonts", "does-not-exist.flf")));
}

[TestMethod]
public async Task FigletTextStyle_Equality_DifferentFontPath()
{
// Copy the bundled font to a temp location so equality has two distinct, valid
// .flf paths to compare. TempFileManager cleans up the copy at test teardown.
var tempPath = TempFiles.GetTempFile();
File.Copy(BundledFontPath, tempPath, overwrite: true);

var a = await FigletTextStyle.CreateAsync(BundledFontPath);
var b = await FigletTextStyle.CreateAsync(tempPath);

Assert.AreNotEqual(a, b);
}
}
}
131 changes: 124 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,123 @@ 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 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).
DisplayOptions options = new();
options.Headers[0] = FigletTextStyle.Create(
justification: TextJustification.Left,
foreground: TextColor.Red);

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

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

AssertCrossPlatStringMatch(expected, ConsoleUnderTest.Output);
}

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

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

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

AssertCrossPlatStringMatch(expected, ConsoleUnderTest.Output);
}

[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 async Task RendererTests_FigletFontPathLoadsCustomFont()
{
// When a FigletTextStyle is created with a custom .flf font, the renderer should
// use that font 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() { await FigletTextStyle.CreateAsync(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 +657,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