From 2cfcc2fb1f96b380436ae8283ebc477ffeb61f12 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:09:50 +0100 Subject: [PATCH 1/3] Add `PrintTable` extension for `IAnsiConsole` with unit tests - Implemented `PrintTable` method for rendering tabular data in the console. - Added support for configurable columns, titles, and widths using `TableColumnDef`. - Included comprehensive unit tests for various scenarios, such as empty items, header visibility, and table configuration. --- .../AnsiConsoleExtensions.cs | 47 +++++- .../TableColumnDef.cs | 22 +++ .../AnsiConsoleExtensionsTests.cs | 159 ++++++++++++++++++ 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs create mode 100644 tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index 97fab628..ff857923 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -1,11 +1,13 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using CreativeCoders.Core; using JetBrains.Annotations; using Spectre.Console; namespace CreativeCoders.SysConsole.Core; -[ExcludeFromCodeCoverage] [PublicAPI] public static class AnsiConsoleExtensions { @@ -21,6 +23,7 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c return new AnsiConsolePrint(ansiConsole); } + [PublicAPI] public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -31,6 +34,7 @@ public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Colo return ansiConsole; } + [PublicAPI] public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -40,4 +44,43 @@ public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, return ansiConsole; } + + [PublicAPI] + public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable items, + TableColumnDef[] columns, Action? configureTable = null) + { + var table = new Table + { + ShowHeaders = false, + Border = TableBorder.None + }; + + foreach (var tableColumnDef in columns) + { + table.AddColumn(tableColumnDef.Title ?? string.Empty, x => + { + x.Width = tableColumnDef.Width; + tableColumnDef.ConfigureColumn(x); + }); + + if (!string.IsNullOrWhiteSpace(tableColumnDef.Title)) + { + table.ShowHeaders = true; + } + } + + configureTable?.Invoke(table); + + if (table.ShowHeaders) + { + table.AddRow(columns.Select(x => new string('=', x.Title?.Length ?? 0)).ToArray()); + } + + foreach (var item in items) + { + table.AddRow(columns.Select(x => x.GetValue(item)).ToArray()); + } + + ansiConsole.Write(table); + } } diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs new file mode 100644 index 00000000..dd947a98 --- /dev/null +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs @@ -0,0 +1,22 @@ +using System; +using CreativeCoders.Core; +using Spectre.Console; + +namespace CreativeCoders.SysConsole.Core; + +public class TableColumnDef( + Func valueSelector, + string? title = null, + int? width = null, + Action? configureColumn = null) +{ + private readonly Func _valueSelector = Ensure.NotNull(valueSelector); + + public string GetValue(T item) => _valueSelector(item)?.ToString() ?? string.Empty; + + public void ConfigureColumn(TableColumn column) => configureColumn?.Invoke(column); + + public string? Title { get; } = title; + + public int? Width { get; } = width; +} diff --git a/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs b/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs new file mode 100644 index 00000000..3d73a0a5 --- /dev/null +++ b/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs @@ -0,0 +1,159 @@ +using AwesomeAssertions; +using CreativeCoders.SysConsole.Core; +using FakeItEasy; +using Spectre.Console; +using Spectre.Console.Rendering; +using Xunit; + +namespace CreativeCoders.SysConsole.UnitTests; + +public class AnsiConsoleExtensionsTests +{ + [Fact] + public void PrintTable_ItemsAndColumns_TableIsWrittenToConsole() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 }, + new TestItem { Name = "Item2", Value = 20 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name"), + new TableColumnDef(x => x.Value, "Value") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(3); // 1 header separator row + 2 data rows + capturedTable.ShowHeaders.Should().BeTrue(); + } + + [Fact] + public void PrintTable_NoColumnTitles_HeadersAreNotShown() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name) + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.ShowHeaders.Should().BeFalse(); + capturedTable.Rows.Count.Should().Be(1); // Only data row + } + + [Fact] + public void PrintTable_WithConfigureTable_TableIsConfigured() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns, table => table.Title = new TableTitle("TestTitle")); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Title!.Text.Should().Be("TestTitle"); + } + + [Fact] + public void PrintTable_WithColumnWidth_ColumnHasSpecifiedWidth() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name", width: 20) + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Columns[0].Width.Should().Be(20); + } + + [Fact] + public void PrintTable_EmptyItems_OnlyHeaderRowsAreWritten() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = Enumerable.Empty(); + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(1); // Only header separator row + } + + private class TestItem + { + public string Name { get; set; } = string.Empty; + + public int Value { get; set; } + } +} From 4414606a71425307e6927c37406c1981ea56522a Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:06:28 +0100 Subject: [PATCH 2/3] Add support for colored table column titles and values in `TableColumnDef` - Introduced `Color` property to `TableColumnDef`. - Enhanced title and value rendering with color formatting. - Refactored `GetValue` and `GetTitle` methods to apply color if specified. --- .../AnsiConsoleExtensions.cs | 2 +- .../TableColumnDef.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index ff857923..19531e17 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -57,7 +57,7 @@ public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable i foreach (var tableColumnDef in columns) { - table.AddColumn(tableColumnDef.Title ?? string.Empty, x => + table.AddColumn(tableColumnDef.GetTitle(), x => { x.Width = tableColumnDef.Width; tableColumnDef.ConfigureColumn(x); diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs index dd947a98..f7d27d82 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/TableColumnDef.cs @@ -8,14 +8,25 @@ public class TableColumnDef( Func valueSelector, string? title = null, int? width = null, + Color? color = null, Action? configureColumn = null) { private readonly Func _valueSelector = Ensure.NotNull(valueSelector); - public string GetValue(T item) => _valueSelector(item)?.ToString() ?? string.Empty; + public string GetValue(T item) => + GetStringWithColor(_valueSelector(item)?.ToString() ?? string.Empty); public void ConfigureColumn(TableColumn column) => configureColumn?.Invoke(column); + private string GetStringWithColor(string text) + { + return color == null + ? text + : $"[{color.Value.ToMarkup()}]{text}[/]"; + } + + public string GetTitle() => GetStringWithColor(Title ?? string.Empty); + public string? Title { get; } = title; public int? Width { get; } = width; From 100fc12704541848ea73191ce3883f5cec3b4bea Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:30:01 +0100 Subject: [PATCH 3/3] Add unit tests for `PrintTable` with colored and non-colored columns - Introduce tests to verify table rendering with and without column colors. - Suppress nullable warnings in test class with `[SuppressMessage]`. - Refactor `TestItem` class to use immutable properties with `init`. --- .../AnsiConsoleExtensionsTests.cs | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs b/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs index 3d73a0a5..cfa7d695 100644 --- a/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs +++ b/tests/CreativeCoders.SysConsole.UnitTests/AnsiConsoleExtensionsTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using AwesomeAssertions; using CreativeCoders.SysConsole.Core; using FakeItEasy; @@ -7,6 +8,7 @@ namespace CreativeCoders.SysConsole.UnitTests; +[SuppressMessage("ReSharper", "NullableWarningSuppressionIsUsed")] public class AnsiConsoleExtensionsTests { [Fact] @@ -150,10 +152,100 @@ public void PrintTable_EmptyItems_OnlyHeaderRowsAreWritten() capturedTable!.Rows.Count.Should().Be(1); // Only header separator row } + [Fact] + public void PrintTable_ColoredColumns_TableIsWrittenWithColorToConsole() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name", color: Color.Red), + new TableColumnDef(x => x.Value, "Value", color: Color.Green) + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(2); + + var row = (capturedTable.Rows as IReadOnlyList)[1]; + + var col0 = row[0].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col0.Text + .Should().Be("Item1"); + + col0.Style.Foreground + .Should().Be(Color.Red); + + var col1 = row[1].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col1.Text + .Should().Be("10"); + + col1.Style.Foreground + .Should().Be(Color.Green); + } + + [Fact] + public void PrintTable_NotColoredColumns_TableIsWrittenWithOutColorToConsole() + { + // Arrange + var ansiConsole = A.Fake(); + + var items = new[] + { + new TestItem { Name = "Item1", Value = 10 } + }; + + var columns = new[] + { + new TableColumnDef(x => x.Name, "Name"), + new TableColumnDef(x => x.Value, "Value") + }; + + Table? capturedTable = null; + A.CallTo(() => ansiConsole.Write(A.Ignored)) + .Invokes(call => capturedTable = call.Arguments.Get
(0)); + + // Act + ansiConsole.PrintTable(items, columns); + + // Assert + capturedTable.Should().NotBeNull(); + capturedTable!.Rows.Count.Should().Be(2); + + var row = (capturedTable.Rows as IReadOnlyList)[1]; + + var col0 = row[0].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col0.Text + .Should().Be("Item1"); + + col0.Style.Foreground + .Should().Be(Color.Default); + + var col1 = row[1].GetSegments(AnsiConsole.Create(new AnsiConsoleSettings())).First(); + col1.Text + .Should().Be("10"); + + col1.Style.Foreground + .Should().Be(Color.Default); + } + private class TestItem { - public string Name { get; set; } = string.Empty; + public string Name { get; init; } = string.Empty; - public int Value { get; set; } + public int Value { get; init; } } }