Fixes #4289 - Simplify Drawing/Color: unify named color handling under StandardColor and remove layered resolvers (#4432)

* Initial plan

* Delete AnsiColorNameResolver and MultiStandardColorNameResolver, add legacy 16-color names to StandardColor

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Refactor and enhance tests for Color, Region, and Lines

Refactored `Color` struct by removing unused methods and simplifying logic. Updated namespaces for better organization. Enhanced test coverage for `Color`, `Region`, and `LineCanvas` with new test cases, parameterized tests, and edge case handling.

Added `StraightLineExtensionsTests`, `StraightLineTests`, and `RegionClassTests` to validate behavior under various scenarios. Improved `MergeRectangles` stability and addressed crash patterns. Removed legacy features and unused code. Enhanced documentation and optimized performance in key methods.

* Improve Color struct and StandardColors functionality

Enhanced the Color struct to fully support the alpha channel for rendering intent while maintaining semantic color identity. Updated TryNameColor to ignore alpha when matching colors, ensuring transparency does not affect color resolution. Expanded XML documentation to clarify alpha channel usage and future alpha blending support.

Improved drawing documentation to explain the lifecycle, deferred rendering, and color support, including 24-bit true color and legacy 16-color compatibility. Added a new section on transparency and its role in rendering.

Revised StandardColors implementation to use modern C# features and ensure consistent ARGB mapping. Added comprehensive tests for StandardColors and Color, covering alpha handling, color parsing, thread safety, and aliased color resolution. Removed outdated tests relying on legacy behavior.

Enhanced code readability, maintainability, and test coverage to ensure correctness and backward compatibility.

* Code cleanup

* Code cleanup

* Fix warnings. Code cleanup

* Add comprehensive unit tests for ColorStrings class

Introduced a new test class `ColorStringsTests` under the
`DrawingTests.ColorTests` namespace to validate the functionality
of the `ColorStrings` class.

Key changes include:
- Added tests for `GetColorName` to verify behavior for standard
  and non-standard colors, ignoring alpha channels, and handling
  known colors.
- Added tests for `GetStandardColorNames` to ensure the method
  returns a non-empty, alphabetically sorted collection containing
  all `StandardColor` enum values.
- Implemented tests for `TryParseStandardColorName` to validate
  case-insensitive parsing, hex color support, handling invalid
  input, and `ReadOnlySpan<char>` compatibility.
- Added tests for `TryParseNamedColor` to verify parsing of named
  and hex colors, handling of aliases, and `ReadOnlySpan<char>`
  support.
- Added round-trip tests to ensure consistency between
  `GetColorName`, `TryParseNamedColor`, `GetStandardColorNames`,
  and `TryParseStandardColorName`.

These tests ensure robust validation of color parsing and naming
functionality.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Copilot
2025-12-03 11:09:02 -07:00
committed by GitHub
parent c9868e9901
commit 6d53276be2
43 changed files with 951 additions and 835 deletions

View File

@@ -9,7 +9,7 @@ namespace ApplicationTests.Timeout;
/// These tests verify that timeouts fire correctly, can be added/removed,
/// handle exceptions properly, and work with Application.Run() calls.
/// </summary>
public class TimeoutTests (ITestOutputHelper output)
public class TimeoutTests
{
[Fact]
public void AddTimeout_Callback_Can_Add_New_Timeout ()

View File

@@ -52,7 +52,7 @@ public class ColorJsonConverterTests
[InlineData (ColorName16.Red, "Red")]
[InlineData (ColorName16.Magenta, "Fuchsia")] // W3C+ Standard overrides
[InlineData (ColorName16.Yellow, "Yellow")]
[InlineData (ColorName16.DarkGray, "DarkGray")]
[InlineData (ColorName16.DarkGray, "BrightBlack")] // Legacy ColorName16.DarkGray now serializes as BrightBlack (first alphabetical match)
[InlineData (ColorName16.BrightBlue, "BrightBlue")]
[InlineData (ColorName16.BrightGreen, "BrightGreen")]
[InlineData (ColorName16.BrightCyan, "BrightCyan")]
@@ -98,7 +98,7 @@ public class ColorJsonConverterTests
[InlineData ("BrightYellow", Color.BrightYellow)]
[InlineData ("Yellow", Color.Yellow)]
[InlineData ("Cyan", Color.Cyan)]
[InlineData ("DarkGray", Color.DarkGray)]
[InlineData ("BrightBlack", Color.DarkGray)] // Legacy ColorName16.DarkGray is now accessible as BrightBlack
[InlineData ("Gray", Color.Gray)]
[InlineData ("Green", Color.Green)]
[InlineData ("Magenta", Color.Magenta)]
@@ -113,7 +113,7 @@ public class ColorJsonConverterTests
var actualColor = JsonSerializer.Deserialize<Color> (json, JsonOptions);
// Assert
Assert.Equal (new Color (expectedColor), actualColor);
Assert.Equal (new (expectedColor), actualColor);
}
[Fact]

View File

@@ -12,7 +12,7 @@ public class SourcesManagerTests
// Arrange
var sourcesManager = new SourcesManager ();
var stream = new MemoryStream ();
var source = "test.json";
var source = "Load_WithNullSettingsScope_ReturnsFalse";
var location = ConfigLocations.AppCurrent;
// Act
@@ -37,7 +37,7 @@ public class SourcesManagerTests
}
""";
var location = ConfigLocations.HardCoded;
var source = "stream";
var source = "Load_WithValidStream_UpdatesSettingsScope";
var stream = new MemoryStream ();
var writer = new StreamWriter (stream);
@@ -69,7 +69,7 @@ public class SourcesManagerTests
writer.Flush ();
stream.Position = 0;
var source = "test.json";
var source = "Load_WithInvalidJson_AddsJsonError";
var location = ConfigLocations.AppCurrent;
// Act
@@ -180,7 +180,7 @@ public class SourcesManagerTests
var sourcesManager = new SourcesManager ();
var settingsScope = new SettingsScope ();
var source = "test.json";
var source = "Load_WithNullOrEmptyJson_ReturnsFalse";
var location = ConfigLocations.AppCurrent;
// Act
@@ -206,7 +206,7 @@ public class SourcesManagerTests
"Application.QuitKey": "Ctrl+Z"
}
""";
var source = "test.json";
var source = "Load_WithValidJson_UpdatesSettingsScope";
var location = ConfigLocations.HardCoded;
// Act
@@ -233,7 +233,7 @@ public class SourcesManagerTests
// "Button.DefaultShadowStyle": "None"
// }
// """;
// var source = "test.json";
// var source = "Update_WithValidJson_UpdatesThemeScope";
// var location = ConfigLocations.HardCoded;
// // Act

View File

@@ -1,168 +0,0 @@
#nullable enable
namespace DrawingTests;
public class AnsiColorNameResolverTests
{
private readonly AnsiColorNameResolver _candidate = new ();
[Fact]
public void TryNameColor_Resolves_All_ColorName16 ()
{
var resolver = new AnsiColorNameResolver ();
foreach (ColorName16 name in Enum.GetValues<ColorName16> ())
{
var color = new Color (name);
bool success = resolver.TryNameColor (color, out string? resultName);
Assert.True (success, $"Expected TryNameColor to succeed for {name}");
Assert.Equal (name.ToString (), resultName);
}
}
[Fact]
public void TryParseColor_Resolves_All_ColorName16_Names ()
{
var resolver = new AnsiColorNameResolver ();
foreach (ColorName16 name in Enum.GetValues<ColorName16> ())
{
bool success = resolver.TryParseColor (name.ToString (), out Color parsed);
Assert.True (success, $"Expected TryParseColor to succeed for {name}");
Assert.Equal (new Color (name), parsed);
}
}
public static IEnumerable<object []> AnsiColorName16NumericValues =>
Enum.GetValues<ColorName16> ()
.Select (e => new object [] { ((int)e).ToString () });
[Theory]
[MemberData (nameof (AnsiColorName16NumericValues))]
public void TryParseColor_Accepts_Enum_UnderlyingNumbers (string numeric)
{
var resolver = new AnsiColorNameResolver ();
bool success = resolver.TryParseColor (numeric, out _);
Assert.True (success, $"Expected numeric enum value '{numeric}' to resolve successfully.");
}
[Fact]
public void GetNames_Returns16ColorNames ()
{
string [] expected = Enum.GetNames<ColorName16> ();
string [] actual = _candidate.GetColorNames ().ToArray ();
Assert.Equal (expected, actual);
}
[Theory]
[InlineData (0, 0, 0, nameof (ColorName16.Black))]
[InlineData (0, 0, 255, nameof (ColorName16.Blue))]
[InlineData (59, 120, 255, nameof (ColorName16.BrightBlue))]
[InlineData (97, 214, 214, nameof (ColorName16.BrightCyan))]
[InlineData (22, 198, 12, nameof (ColorName16.BrightGreen))]
[InlineData (180, 0, 158, nameof (ColorName16.BrightMagenta))]
[InlineData (231, 72, 86, nameof (ColorName16.BrightRed))]
[InlineData (249, 241, 165, nameof (ColorName16.BrightYellow))]
[InlineData (0, 255, 255, nameof (ColorName16.Cyan))]
[InlineData (118, 118, 118, nameof (ColorName16.DarkGray))]
[InlineData (128, 128, 128, nameof (ColorName16.Gray))]
[InlineData (0, 128, 0, nameof (ColorName16.Green))]
[InlineData (255, 0, 255, nameof (ColorName16.Magenta))]
[InlineData (255, 0, 0, nameof (ColorName16.Red))]
[InlineData (255, 255, 255, nameof (ColorName16.White))]
[InlineData (255, 255, 0, nameof (ColorName16.Yellow))]
public void TryNameColor_ReturnsExpectedColorName (byte r, byte g, byte b, string expectedName)
{
var expected = (true, expectedName);
bool actualSuccess = _candidate.TryNameColor (new Color (r, g, b), out string? actualName);
var actual = (actualSuccess, actualName);
Assert.Equal (expected, actual);
}
[Fact]
public void TryNameColor_NoMatchFails ()
{
(bool, string?) expected = (false, null);
bool actualSuccess = _candidate.TryNameColor (new Color (1, 2, 3), out string? actualName);
var actual = (actualSuccess, actualName);
Assert.Equal (expected, actual);
}
[Theory]
[InlineData (nameof (ColorName16.Black), 0, 0, 0)]
[InlineData (nameof (ColorName16.Blue), 0, 0, 255)]
[InlineData (nameof (ColorName16.BrightBlue), 59, 120, 255)]
[InlineData (nameof (ColorName16.BrightCyan), 97, 214, 214)]
[InlineData (nameof (ColorName16.BrightGreen), 22, 198, 12)]
[InlineData (nameof (ColorName16.BrightMagenta), 180, 0, 158)]
[InlineData (nameof (ColorName16.BrightRed), 231, 72, 86)]
[InlineData (nameof (ColorName16.BrightYellow), 249, 241, 165)]
[InlineData (nameof (ColorName16.Cyan), 0, 255, 255)]
[InlineData (nameof (ColorName16.DarkGray), 118, 118, 118)]
[InlineData (nameof (ColorName16.Gray), 128, 128, 128)]
[InlineData (nameof (ColorName16.Green), 0, 128, 0)]
[InlineData (nameof (ColorName16.Magenta), 255, 0, 255)]
[InlineData (nameof (ColorName16.Red), 255, 0, 0)]
[InlineData (nameof (ColorName16.White), 255, 255, 255)]
[InlineData (nameof (ColorName16.Yellow), 255, 255, 0)]
// Case-insensitive
[InlineData ("BRIGHTBLUE", 59, 120, 255)]
[InlineData ("brightblue", 59, 120, 255)]
public void TryParseColor_ReturnsExpectedColor (string inputName, byte r, byte g, byte b)
{
var expected = (true, new Color (r, g, b));
bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor);
var actual = (actualSuccess, actualColor);
Assert.Equal (expected, actual);
}
[Theory]
[InlineData ("12", 231, 72, 86)] // ColorName16.BrightRed
public void TryParseColor_ResolvesValidEnumNumber (string inputName, byte r, byte g, byte b)
{
var expected = (true, new Color (r, g, b));
bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor);
var actual = (actualSuccess, actualColor);
Assert.Equal (expected, actual);
}
[Theory]
[InlineData (null)]
[InlineData ("")]
[InlineData ("brightlight")]
public void TryParseColor_FailsOnInvalidColorName (string? invalidName)
{
var expected = (false, default (Color));
bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor);
var actual = (actualSuccess, actualColor);
Assert.Equal (expected, actual);
}
[Theory]
[InlineData ("-12")]
public void TryParseColor_FailsOnInvalidEnumNumber (string invalidName)
{
var expected = (false, default (Color));
bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor);
var actual = (actualSuccess, actualColor);
Assert.Equal (expected, actual);
}
}

View File

@@ -1,6 +1,6 @@
namespace DrawingTests;
namespace DrawingTests.ColorTests;
public partial class ColorTests
public partial class ColorClassTests
{
[Fact]
public void Constructor_Empty_ReturnsColorWithZeroValue ()

View File

@@ -1,9 +1,9 @@
using System.Numerics;
using System.Reflection;
namespace DrawingTests;
namespace DrawingTests.ColorTests;
public partial class ColorTests
public partial class ColorClassTests
{
[Theory]
[Trait ("Category", "Operators")]

View File

@@ -2,9 +2,9 @@
using System.Buffers.Binary;
using System.Globalization;
namespace DrawingTests;
namespace DrawingTests.ColorTests;
public partial class ColorTests
public partial class ColorClassTests
{
[Fact]
public void Color_ToString_WithNamedColor ()

View File

@@ -1,9 +1,9 @@
using System.Numerics;
using System.Runtime.CompilerServices;
namespace DrawingTests;
namespace DrawingTests.ColorTests;
public partial class ColorTests
public partial class ColorClassTests
{
[Fact]
[Trait ("Category", "Type Checks")]

View File

@@ -1,8 +1,7 @@
#nullable enable

namespace DrawingTests.ColorTests;
namespace DrawingTests;
public partial class ColorTests
public partial class ColorClassTests
{
[Theory]
[CombinatorialData]

View File

@@ -1,27 +0,0 @@
using Xunit;
namespace DrawingTests;
public class ColorStandardColorTests
{
[Fact]
public void ToString_Returns_Standard_Name_For_StandardColor_CadetBlue()
{
// Without the fix, this uses Color(in StandardColor) -> this((int)colorName),
// which sets A=0x00 and prevents name resolution (expects A=0xFF).
var c = new Terminal.Gui.Drawing.Color(Terminal.Gui.Drawing.StandardColor.CadetBlue);
// Expected: named color
Assert.Equal("CadetBlue", c.ToString());
}
[Fact]
public void ToString_G_Prints_Opaque_ARGB_For_StandardColor_CadetBlue()
{
// Without the fix, A=0x00, so "G" prints "#005F9EA0" instead of "#FF5F9EA0".
var c = new Terminal.Gui.Drawing.Color(Terminal.Gui.Drawing.StandardColor.CadetBlue);
// Expected: #AARRGGBB with A=FF (opaque)
Assert.Equal("#FF5F9EA0", c.ToString("G", null));
}
}

View File

@@ -0,0 +1,326 @@
namespace DrawingTests.ColorTests;
public class ColorStringsTests
{
[Fact]
public void GetColorName_ReturnsNameForStandardColor ()
{
Color red = new (255, 0);
string? name = ColorStrings.GetColorName (red);
Assert.Equal ("Red", name);
}
[Fact]
public void GetColorName_ReturnsNullForNonStandardColor ()
{
Color custom = new (1, 2, 3);
string? name = ColorStrings.GetColorName (custom);
Assert.Null (name);
}
[Fact]
public void GetColorName_IgnoresAlphaChannel ()
{
Color opaqueRed = new (255, 0, 0, 255);
Color transparentRed = new (255, 0, 0, 128);
Color fullyTransparentRed = new (255, 0, 0, 0);
string? name1 = ColorStrings.GetColorName (opaqueRed);
string? name2 = ColorStrings.GetColorName (transparentRed);
string? name3 = ColorStrings.GetColorName (fullyTransparentRed);
Assert.Equal ("Red", name1);
Assert.Equal ("Red", name2);
Assert.Equal ("Red", name3);
}
[Theory]
[InlineData (240, 248, 255, "AliceBlue")]
[InlineData (0, 255, 255, "Aqua")]
[InlineData (0, 0, 0, "Black")]
[InlineData (0, 0, 255, "Blue")]
[InlineData (0, 128, 0, "Green")]
[InlineData (255, 0, 0, "Red")]
[InlineData (255, 255, 255, "White")]
[InlineData (255, 255, 0, "Yellow")]
public void GetColorName_ReturnsCorrectNameForKnownColors (int r, int g, int b, string expectedName)
{
Color color = new (r, g, b);
string? name = ColorStrings.GetColorName (color);
Assert.Equal (expectedName, name);
}
[Fact]
public void GetStandardColorNames_ReturnsNonEmptyCollection ()
{
IEnumerable<string> names = ColorStrings.GetStandardColorNames ();
Assert.NotNull (names);
Assert.NotEmpty (names);
}
[Fact]
public void GetStandardColorNames_ReturnsAlphabeticallySortedNames ()
{
IEnumerable<string> names = ColorStrings.GetStandardColorNames ();
string [] namesArray = names.ToArray ();
string [] sortedNames = namesArray.OrderBy (n => n).ToArray ();
Assert.Equal (sortedNames, namesArray);
}
[Fact]
public void GetStandardColorNames_ContainsKnownColors ()
{
IEnumerable<string> names = ColorStrings.GetStandardColorNames ();
string [] namesArray = names.ToArray ();
Assert.Contains ("Red", namesArray);
Assert.Contains ("Green", namesArray);
Assert.Contains ("Blue", namesArray);
Assert.Contains ("White", namesArray);
Assert.Contains ("Black", namesArray);
Assert.Contains ("AliceBlue", namesArray);
Assert.Contains ("Tomato", namesArray);
}
[Fact]
public void GetStandardColorNames_ContainsAllStandardColorEnumValues ()
{
IEnumerable<string> names = ColorStrings.GetStandardColorNames ();
string [] namesArray = names.ToArray ();
string [] enumNames = Enum.GetNames<StandardColor> ();
// All enum names should be in the returned collection
foreach (string enumName in enumNames)
{
Assert.Contains (enumName, namesArray);
}
// The counts should match
Assert.Equal (enumNames.Length, namesArray.Length);
}
[Theory]
[InlineData ("Red")]
[InlineData ("red")]
[InlineData ("RED")]
[InlineData ("Green")]
[InlineData ("green")]
[InlineData ("Blue")]
[InlineData ("AliceBlue")]
[InlineData ("aliceblue")]
[InlineData ("ALICEBLUE")]
public void TryParseStandardColorName_ParsesValidColorNamesCaseInsensitively (string colorName)
{
bool result = ColorStrings.TryParseStandardColorName (colorName, out Color color);
Assert.True (result);
Assert.NotEqual (default (Color), color);
}
[Theory]
[InlineData ("Red", 255, 0, 0)]
[InlineData ("Green", 0, 128, 0)]
[InlineData ("Blue", 0, 0, 255)]
[InlineData ("White", 255, 255, 255)]
[InlineData ("Black", 0, 0, 0)]
[InlineData ("AliceBlue", 240, 248, 255)]
[InlineData ("Tomato", 255, 99, 71)]
public void TryParseStandardColorName_ParsesCorrectRgbValues (string colorName, int expectedR, int expectedG, int expectedB)
{
bool result = ColorStrings.TryParseStandardColorName (colorName, out Color color);
Assert.True (result);
Assert.Equal (expectedR, color.R);
Assert.Equal (expectedG, color.G);
Assert.Equal (expectedB, color.B);
}
[Theory]
[InlineData ("#FF0000", 255, 0, 0)]
[InlineData ("#00FF00", 0, 255, 0)]
[InlineData ("#0000FF", 0, 0, 255)]
[InlineData ("#FFFFFF", 255, 255, 255)]
[InlineData ("#000000", 0, 0, 0)]
[InlineData ("#F0F8FF", 240, 248, 255)]
public void TryParseStandardColorName_ParsesHexColorFormat (string hexColor, int expectedR, int expectedG, int expectedB)
{
bool result = ColorStrings.TryParseStandardColorName (hexColor, out Color color);
Assert.True (result);
Assert.Equal (expectedR, color.R);
Assert.Equal (expectedG, color.G);
Assert.Equal (expectedB, color.B);
}
[Theory]
[InlineData ("#ff0000")]
[InlineData ("#FF0000")]
[InlineData ("#Ff0000")]
public void TryParseStandardColorName_ParsesHexColorCaseInsensitively (string hexColor)
{
bool result = ColorStrings.TryParseStandardColorName (hexColor, out Color color);
Assert.True (result);
Assert.Equal (255, color.R);
Assert.Equal (0, color.G);
Assert.Equal (0, color.B);
}
[Theory]
[InlineData ("")]
[InlineData ("NotAColor")]
[InlineData ("Invalid")]
[InlineData ("123")]
[InlineData ("#FF")]
[InlineData ("#FFFF")]
[InlineData ("#FFFFFFFF")]
[InlineData ("FF0000")]
public void TryParseStandardColorName_ReturnsFalseForInvalidInput (string invalidInput)
{
bool result = ColorStrings.TryParseStandardColorName (invalidInput, out Color color);
Assert.False (result);
Assert.Equal (default (Color), color);
}
[Fact]
public void TryParseStandardColorName_SetsAlphaToFullyOpaque ()
{
bool result = ColorStrings.TryParseStandardColorName ("Red", out Color color);
Assert.True (result);
Assert.Equal (255, color.A);
}
[Fact]
public void TryParseStandardColorName_WorksWithReadOnlySpan ()
{
ReadOnlySpan<char> span = "Red".AsSpan ();
bool result = ColorStrings.TryParseStandardColorName (span, out Color color);
Assert.True (result);
Assert.Equal (255, color.R);
Assert.Equal (0, color.G);
Assert.Equal (0, color.B);
}
[Theory]
[InlineData ("Red")]
[InlineData ("Green")]
[InlineData ("Blue")]
[InlineData ("AliceBlue")]
[InlineData ("#FF0000")]
public void TryParseNamedColor_ParsesValidColorNames (string colorName)
{
bool result = ColorStrings.TryParseNamedColor (colorName, out Color color);
Assert.True (result);
Assert.NotEqual (default (Color), color);
}
[Theory]
[InlineData ("Red", 255, 0, 0)]
[InlineData ("Green", 0, 128, 0)]
[InlineData ("Blue", 0, 0, 255)]
[InlineData ("#FF0000", 255, 0, 0)]
[InlineData ("#00FF00", 0, 255, 0)]
public void TryParseNamedColor_ParsesCorrectRgbValues (string colorName, int expectedR, int expectedG, int expectedB)
{
bool result = ColorStrings.TryParseNamedColor (colorName, out Color color);
Assert.True (result);
Assert.Equal (expectedR, color.R);
Assert.Equal (expectedG, color.G);
Assert.Equal (expectedB, color.B);
}
[Theory]
[InlineData ("")]
[InlineData ("NotAColor")]
[InlineData ("Invalid")]
[InlineData ("#ZZ0000")]
public void TryParseNamedColor_ReturnsFalseForInvalidInput (string invalidInput)
{
bool result = ColorStrings.TryParseNamedColor (invalidInput, out Color color);
Assert.False (result);
Assert.Equal (default (Color), color);
}
[Fact]
public void TryParseNamedColor_WorksWithReadOnlySpan ()
{
ReadOnlySpan<char> span = "Blue".AsSpan ();
bool result = ColorStrings.TryParseNamedColor (span, out Color color);
Assert.True (result);
Assert.Equal (0, color.R);
Assert.Equal (0, color.G);
Assert.Equal (255, color.B);
}
[Theory]
[InlineData (nameof (StandardColor.Aqua), nameof (StandardColor.Cyan))]
[InlineData (nameof (StandardColor.Fuchsia), nameof (StandardColor.Magenta))]
public void TryParseNamedColor_HandlesColorAliases (string name1, string name2)
{
bool result1 = ColorStrings.TryParseNamedColor (name1, out Color color1);
bool result2 = ColorStrings.TryParseNamedColor (name2, out Color color2);
Assert.True (result1);
Assert.True (result2);
Assert.Equal (color1.R, color2.R);
Assert.Equal (color1.G, color2.G);
Assert.Equal (color1.B, color2.B);
}
[Fact]
public void GetColorName_And_TryParseNamedColor_RoundTrip ()
{
// Get a standard color name
Color originalColor = new (255, 0);
string? colorName = ColorStrings.GetColorName (originalColor);
Assert.NotNull (colorName);
// Parse it back
bool result = ColorStrings.TryParseNamedColor (colorName, out Color parsedColor);
Assert.True (result);
Assert.Equal (originalColor.R, parsedColor.R);
Assert.Equal (originalColor.G, parsedColor.G);
Assert.Equal (originalColor.B, parsedColor.B);
}
[Fact]
public void GetStandardColorNames_And_TryParseStandardColorName_RoundTrip ()
{
// Get all standard color names
IEnumerable<string> names = ColorStrings.GetStandardColorNames ();
// Each name should parse successfully
foreach (string name in names)
{
bool result = ColorStrings.TryParseStandardColorName (name, out Color color);
Assert.True (result, $"Failed to parse standard color name: {name}");
// And should get the same name back (for non-aliases)
string? retrievedName = ColorStrings.GetColorName (color);
Assert.NotNull (retrievedName);
// The retrieved name should be one of the valid names for this color
// (could be different if there are aliases)
Assert.True (
ColorStrings.TryParseStandardColorName (retrievedName, out Color retrievedColor),
$"Retrieved name '{retrievedName}' should be parseable"
);
Assert.Equal (color.R, retrievedColor.R);
Assert.Equal (color.G, retrievedColor.G);
Assert.Equal (color.B, retrievedColor.B);
}
}
}

View File

@@ -1,156 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Xunit.Abstractions;
using Terminal.Gui;
namespace DrawingTests;
public class MultiStandardColorNameResolverTests (ITestOutputHelper output)
{
private readonly MultiStandardColorNameResolver _candidate = new ();
public static IEnumerable<object []> StandardColors =>
Enum.GetValues<StandardColor> ().Select (sc => new object [] { sc });
[Theory]
[MemberData (nameof (StandardColors))]
public void TryParseColor_ResolvesAllStandardColorNames (StandardColor standardColor)
{
string name = standardColor.ToString ();
bool parsed = _candidate.TryParseColor (name, out Color actualColor);
Assert.True (parsed, $"TryParseColor should succeed for {name}");
Color expectedColor = new (Terminal.Gui.Drawing.StandardColors.GetArgb (standardColor));
Assert.Equal (expectedColor.R, actualColor.R);
Assert.Equal (expectedColor.G, actualColor.G);
Assert.Equal (expectedColor.B, actualColor.B);
}
[Theory]
[MemberData (nameof (StandardColors))]
public void TryNameColor_ResolvesAllStandardColors (StandardColor standardColor)
{
Color color = new (Terminal.Gui.Drawing.StandardColors.GetArgb (standardColor));
bool success = _candidate.TryNameColor (color, out string? resolvedName);
if (!success)
{
output.WriteLine ($"Unmapped: {standardColor} → {color}");
}
Assert.True (success, $"TryNameColor should succeed for {standardColor}");
List<string> expectedNames = Enum.GetNames<StandardColor> ()
.Where (name => Terminal.Gui.Drawing.StandardColors.GetArgb (Enum.Parse<StandardColor> (name)) == color.Argb)
.ToList ();
Assert.Contains (resolvedName, expectedNames);
}
[Fact]
public void TryNameColor_Logs_Unmapped_StandardColors ()
{
List<StandardColor> unmapped = new ();
foreach (StandardColor sc in Enum.GetValues<StandardColor> ())
{
Color color = new (Terminal.Gui.Drawing.StandardColors.GetArgb (sc));
if (!_candidate.TryNameColor (color, out _))
{
unmapped.Add (sc);
}
}
output.WriteLine ("Unmapped StandardColor entries:");
foreach (StandardColor sc in unmapped.Distinct ())
{
output.WriteLine ($"- {sc}");
}
Assert.True (unmapped.Count < 10, $"Too many StandardColor values are not name-resolvable. Got {unmapped.Count}.");
}
[Theory]
[InlineData (nameof (ColorName16.Black))]
[InlineData (nameof (ColorName16.White))]
[InlineData (nameof (ColorName16.Red))]
[InlineData (nameof (ColorName16.Green))]
[InlineData (nameof (ColorName16.Blue))]
[InlineData (nameof (ColorName16.Cyan))]
[InlineData (nameof (ColorName16.Magenta))]
[InlineData (nameof (ColorName16.DarkGray))]
[InlineData (nameof (ColorName16.BrightGreen))]
[InlineData (nameof (ColorName16.BrightMagenta))]
[InlineData (nameof (StandardColor.AliceBlue))]
[InlineData (nameof (StandardColor.BlanchedAlmond))]
public void GetNames_ContainsKnownNames (string name)
{
string [] names = _candidate.GetColorNames ().ToArray ();
Assert.Contains (name, names);
}
[Theory]
[InlineData (0, 0, 0, nameof (ColorName16.Black))]
[InlineData (0, 0, 255, nameof (ColorName16.Blue))]
[InlineData (59, 120, 255, nameof (ColorName16.BrightBlue))]
[InlineData (255, 0, 0, nameof (ColorName16.Red))]
[InlineData (255, 255, 255, nameof (ColorName16.White))]
[InlineData (240, 248, 255, nameof (StandardColor.AliceBlue))]
[InlineData (178, 34, 34, nameof (StandardColor.FireBrick))]
[InlineData (245, 245, 245, nameof (StandardColor.WhiteSmoke))]
public void TryNameColor_ReturnsExpectedColorNames (byte r, byte g, byte b, string expectedName)
{
Color color = new (r, g, b);
bool actualSuccess = _candidate.TryNameColor (color, out string? actualName);
Assert.True (actualSuccess);
Assert.Equal (expectedName, actualName);
}
[Fact]
public void TryNameColor_NoMatchFails ()
{
Color input = new (1, 2, 3);
bool success = _candidate.TryNameColor (input, out string? actualName);
Assert.False (success);
Assert.Null (actualName);
}
[Theory]
[InlineData ("12", 231, 72, 86)] // ColorName16.BrightRed
[InlineData ("16737095", 255, 99, 71)] // StandardColor.Tomato
[InlineData ("#FF0000", 255, 0, 0)] // Red
public void TryParseColor_ResolvesValidEnumNumber (string inputName, byte r, byte g, byte b)
{
bool success = _candidate.TryParseColor (inputName, out Color actualColor);
Assert.True (success);
Assert.Equal (r, actualColor.R);
Assert.Equal (g, actualColor.G);
Assert.Equal (b, actualColor.B);
}
[Theory]
[InlineData (null)]
[InlineData ("")]
[InlineData ("brightlight")]
public void TryParseColor_FailsOnInvalidColorName (string? input)
{
bool success = _candidate.TryParseColor (input, out Color actualColor);
Assert.False (success);
Assert.Equal (default, actualColor);
}
[Theory]
[InlineData ("-12")]
[InlineData ("-16737095")]
public void TryParseColor_FailsOnInvalidEnumNumber (string input)
{
bool success = _candidate.TryParseColor (input, out Color actualColor);
Assert.False (success);
Assert.Equal (default, actualColor);
}
}

View File

@@ -1,8 +1,6 @@
#nullable enable
using Xunit.Abstractions;
using Xunit.Abstractions;
namespace DrawingTests;
namespace DrawingTests.ColorTests;
public class StandardColorNameResolverTests (ITestOutputHelper output)
{

View File

@@ -0,0 +1,268 @@
#pragma warning disable xUnit1031
namespace DrawingTests.ColorTests;
public class StandardColorsTests
{
[Fact]
public void GetArgb_HandlesAllStandardColorValues ()
{
foreach (StandardColor sc in Enum.GetValues<StandardColor> ())
{
uint argb = StandardColors.GetArgb (sc);
// Verify alpha is always 0xFF (fully opaque)
var alpha = (byte)((argb >> 24) & 0xFF);
Assert.Equal (255, alpha);
// Verify the RGB components match the enum value
var enumRgb = (int)sc;
uint expectedArgb = (uint)enumRgb | 0xFF000000;
Assert.Equal (expectedArgb, argb);
}
}
[Theory]
[InlineData (StandardColor.Red, 255, 0, 0)]
[InlineData (StandardColor.Green, 0, 128, 0)]
[InlineData (StandardColor.Blue, 0, 0, 255)]
[InlineData (StandardColor.White, 255, 255, 255)]
[InlineData (StandardColor.Black, 0, 0, 0)]
[InlineData (StandardColor.AliceBlue, 240, 248, 255)]
[InlineData (StandardColor.YellowGreen, 154, 205, 50)]
public void GetArgb_ReturnsCorrectArgbWithFullAlpha (StandardColor standardColor, byte r, byte g, byte b)
{
uint argb = StandardColors.GetArgb (standardColor);
var actualA = (byte)((argb >> 24) & 0xFF);
var actualR = (byte)((argb >> 16) & 0xFF);
var actualG = (byte)((argb >> 8) & 0xFF);
var actualB = (byte)(argb & 0xFF);
Assert.Equal (255, actualA);
Assert.Equal (r, actualR);
Assert.Equal (g, actualG);
Assert.Equal (b, actualB);
}
[Fact]
public void GetColorNames_ContainsAllStandardColorEnumValues ()
{
string [] enumNames = Enum.GetNames<StandardColor> ().Order ().ToArray ();
IReadOnlyList<string> colorNames = StandardColors.GetColorNames ();
Assert.Equal (enumNames.Length, colorNames.Count);
Assert.Equal (enumNames, colorNames);
}
[Fact]
public void GetColorNames_IsAlphabeticallySorted ()
{
IReadOnlyList<string> colorNames = StandardColors.GetColorNames ();
string [] sortedNames = colorNames.OrderBy (n => n).ToArray ();
Assert.Equal (sortedNames, colorNames);
}
[Fact]
public void LazyInitialization_IsThreadSafe ()
{
// Force initialization by calling GetColorNames multiple times in parallel
Task [] tasks = new Task [10];
for (var i = 0; i < tasks.Length; i++)
{
tasks [i] = Task.Run (() =>
{
IReadOnlyList<string> names = StandardColors.GetColorNames ();
Assert.NotNull (names);
Assert.NotEmpty (names);
}
);
}
Task.WaitAll (tasks);
}
[Fact]
public void MapValueFactory_CreatesConsistentMapping ()
{
// Call TryNameColor multiple times for the same color
var testColor = new Color (255, 0);
bool result1 = StandardColors.TryNameColor (testColor, out string? name1);
bool result2 = StandardColors.TryNameColor (testColor, out string? name2);
Assert.True (result1);
Assert.True (result2);
Assert.Equal (name1, name2);
}
[Fact]
public void ToString_G_Prints_Opaque_ARGB_For_StandardColor_CadetBlue ()
{
// Without the fix, A=0x00, so "G" prints "#005F9EA0" instead of "#FF5F9EA0".
var c = new Color (StandardColor.CadetBlue);
// Expected: #AARRGGBB with A=FF (opaque)
Assert.Equal ("#FF5F9EA0", c.ToString ("G"));
}
[Fact]
public void ToString_Returns_Standard_Name_For_StandardColor_CadetBlue ()
{
// Without the fix, this uses Color(in StandardColor) -> this((int)colorName),
// which sets A=0x00 and prevents name resolution (expects A=0xFF).
var c = new Color (StandardColor.CadetBlue);
// Expected: named color
Assert.Equal ("CadetBlue", c.ToString ());
}
[Fact]
public void TryNameColor_IgnoresAlphaChannel ()
{
var opaqueRed = new Color (255, 0, 0, 255);
var transparentRed = new Color (255, 0, 0, 128);
Assert.True (StandardColors.TryNameColor (opaqueRed, out string? name1));
Assert.True (StandardColors.TryNameColor (transparentRed, out string? name2));
Assert.Equal (name1, name2);
Assert.Equal ("Red", name1);
}
[Fact]
public void TryNameColor_ReturnsConsistentResultsForSameArgb ()
{
List<Color> colors = new ();
// Create multiple Color instances with the same ARGB values
for (var i = 0; i < 5; i++)
{
colors.Add (new (255, 0));
}
HashSet<string?> names = new ();
foreach (Color color in colors)
{
StandardColors.TryNameColor (color, out string? name);
names.Add (name);
}
// All should resolve to the same name
Assert.Single (names);
Assert.Equal ("Red", names.First ());
}
[Fact]
public void TryNameColor_ReturnsFalseForUnknownColor ()
{
var unknownColor = new Color (1, 2, 3);
bool result = StandardColors.TryNameColor (unknownColor, out string? name);
Assert.False (result);
Assert.Null (name);
}
[Fact]
public void TryNameColor_ReturnsFirstAlphabeticalNameForAliasedColors ()
{
// Aqua and Cyan have the same RGB values
var aqua = new Color (0, 255, 255);
Assert.True (StandardColors.TryNameColor (aqua, out string? name));
// Should return the alphabetically first name
Assert.Equal ("Aqua", name);
}
[Theory]
[InlineData (nameof (StandardColor.Aqua), nameof (StandardColor.Cyan))]
[InlineData (nameof (StandardColor.Fuchsia), nameof (StandardColor.Magenta))]
[InlineData (nameof (StandardColor.DarkGray), nameof (StandardColor.DarkGrey))]
[InlineData (nameof (StandardColor.DarkSlateGray), nameof (StandardColor.DarkSlateGrey))]
[InlineData (nameof (StandardColor.DimGray), nameof (StandardColor.DimGrey))]
[InlineData (nameof (StandardColor.Gray), nameof (StandardColor.Grey))]
[InlineData (nameof (StandardColor.LightGray), nameof (StandardColor.LightGrey))]
[InlineData (nameof (StandardColor.LightSlateGray), nameof (StandardColor.LightSlateGrey))]
[InlineData (nameof (StandardColor.SlateGray), nameof (StandardColor.SlateGrey))]
public void TryParseColor_HandlesColorAliases (string name1, string name2)
{
Assert.True (StandardColors.TryParseColor (name1, out Color color1));
Assert.True (StandardColors.TryParseColor (name2, out Color color2));
Assert.Equal (color1, color2);
}
[Theory]
[InlineData (StandardColor.AmberPhosphor, 255, 191, 0)]
[InlineData (StandardColor.GreenPhosphor, 0, 255, 102)]
[InlineData (StandardColor.GuppieGreen, 173, 255, 47)]
public void TryParseColor_HandlesNonW3CColors (StandardColor color, byte r, byte g, byte b)
{
bool result = StandardColors.TryParseColor (color.ToString (), out Color parsedColor);
Assert.True (result);
Assert.Equal (r, parsedColor.R);
Assert.Equal (g, parsedColor.G);
Assert.Equal (b, parsedColor.B);
}
[Fact]
public void TryParseColor_IsCaseInsensitive ()
{
Assert.True (StandardColors.TryParseColor ("RED", out Color upperColor));
Assert.True (StandardColors.TryParseColor ("red", out Color lowerColor));
Assert.True (StandardColors.TryParseColor ("Red", out Color mixedColor));
Assert.Equal (upperColor, lowerColor);
Assert.Equal (upperColor, mixedColor);
Assert.Equal (255, upperColor.R);
Assert.Equal (0, upperColor.G);
Assert.Equal (0, upperColor.B);
}
[Theory]
[InlineData ("")]
[InlineData ("NotAColor")]
[InlineData ("123456")]
[InlineData ("-1")]
public void TryParseColor_ReturnsFalseForInvalidInput (string invalidInput)
{
bool result = StandardColors.TryParseColor (invalidInput, out Color color);
Assert.False (result);
Assert.Equal (default (Color), color);
}
[Fact]
public void TryParseColor_SetsAlphaToFullyOpaque ()
{
Assert.True (StandardColors.TryParseColor ("Red", out Color color));
Assert.Equal (255, color.A);
}
[Fact]
public void TryParseColor_WithEmptySpan_ReturnsFalse ()
{
ReadOnlySpan<char> emptySpan = ReadOnlySpan<char>.Empty;
bool result = StandardColors.TryParseColor (emptySpan, out Color color);
Assert.False (result);
Assert.Equal (default (Color), color);
}
[Fact]
public void TryParseColor_WithReadOnlySpan_WorksCorrectly ()
{
ReadOnlySpan<char> span = "Red".AsSpan ();
bool result = StandardColors.TryParseColor (span, out Color color);
Assert.True (result);
Assert.Equal (255, color.R);
Assert.Equal (0, color.G);
Assert.Equal (0, color.B);
}
}

View File

@@ -2,7 +2,7 @@
using UnitTests;
using Xunit.Abstractions;
namespace DrawingTests;
namespace DrawingTests.Lines;
/// <summary>
/// Pure unit tests for <see cref="LineCanvas"/> that don't require Application.Driver or View context.

View File

@@ -1,7 +1,7 @@
using UnitTests;
using Xunit.Abstractions;
namespace DrawingTests;
namespace UnitTests.Parallelizable.Drawing.Lines;
public class StraightLineExtensionsTests (ITestOutputHelper output)
{

View File

@@ -1,6 +1,6 @@
using Xunit.Abstractions;
namespace DrawingTests;
namespace UnitTests.Parallelizable.Drawing.Lines;
public class StraightLineTests (ITestOutputHelper output)
{

View File

@@ -1,6 +1,6 @@
using Xunit.Sdk;
namespace DrawingTests;
namespace DrawingTests.RegionTests;
public class DifferenceTests
{

View File

@@ -1,7 +1,7 @@
using System.Collections.Concurrent;
using Xunit.Abstractions;
namespace DrawingTests;
namespace DrawingTests.RegionTests;
/// <summary>
/// Tests for <see cref="Region.DrawOuterBoundary"/>.

View File

@@ -1,4 +1,4 @@
namespace DrawingTests;
namespace DrawingTests.RegionTests;
public class MergeRectanglesTests

View File

@@ -1,6 +1,6 @@
namespace DrawingTests;
namespace DrawingTests.RegionTests;
public class RegionTests
public class RegionClassTests
{
[Fact]
public void Clone_CreatesExactCopy ()

View File

@@ -1,4 +1,4 @@
namespace DrawingTests;
namespace DrawingTests.RegionTests;
using Xunit;