Merge branch 'v2_develop' into v2_4382_stringextensions-getcolumns-fix

This commit is contained in:
Tig
2025-11-11 19:32:23 -07:00
committed by GitHub
311 changed files with 14902 additions and 16912 deletions

View File

@@ -1,25 +1,27 @@
namespace UnitTests_Parallelizable.ApplicationTests;
using UnitTests;
public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
namespace UnitTests_Parallelizable.ApplicationTests;
public class StackExtensionsTests : FakeDriverBase
{
[Fact]
public void Stack_Toplevels_Contains ()
public void Stack_topLevels_Contains ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
var comparer = new ToplevelEqualityComparer ();
Assert.True (Toplevels.Contains (new Window { Id = "w2" }, comparer));
Assert.False (Toplevels.Contains (new Toplevel { Id = "top2" }, comparer));
Assert.True (topLevels.Contains (new Window { Id = "w2" }, comparer));
Assert.False (topLevels.Contains (new Toplevel { Id = "top2" }, comparer));
}
[Fact]
public void Stack_Toplevels_CreateToplevels ()
public void Stack_topLevels_CreatetopLevels ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
int index = Toplevels.Count - 1;
int index = topLevels.Count - 1;
foreach (Toplevel top in Toplevels)
foreach (Toplevel top in topLevels)
{
if (top.GetType () == typeof (Toplevel))
{
@@ -33,7 +35,7 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
index--;
}
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("w4", tops [0].Id);
Assert.Equal ("w3", tops [1].Id);
@@ -43,28 +45,28 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
}
[Fact]
public void Stack_Toplevels_FindDuplicates ()
public void Stack_topLevels_FindDuplicates ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
var comparer = new ToplevelEqualityComparer ();
Toplevels.Push (new Toplevel { Id = "w4" });
Toplevels.Push (new Toplevel { Id = "w1" });
topLevels.Push (new Toplevel { Id = "w4" });
topLevels.Push (new Toplevel { Id = "w1" });
Toplevel [] dup = Toplevels.FindDuplicates (comparer).ToArray ();
Toplevel [] dup = topLevels.FindDuplicates (comparer).ToArray ();
Assert.Equal ("w4", dup [0].Id);
Assert.Equal ("w1", dup [^1].Id);
}
[Fact]
public void Stack_Toplevels_MoveNext ()
public void Stack_topLevels_MoveNext ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
Toplevels.MoveNext ();
topLevels.MoveNext ();
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("w3", tops [0].Id);
Assert.Equal ("w2", tops [1].Id);
@@ -74,13 +76,13 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
}
[Fact]
public void Stack_Toplevels_MovePrevious ()
public void Stack_topLevels_MovePrevious ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
Toplevels.MovePrevious ();
topLevels.MovePrevious ();
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("Top", tops [0].Id);
Assert.Equal ("w4", tops [1].Id);
@@ -90,16 +92,16 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
}
[Fact]
public void Stack_Toplevels_MoveTo ()
public void Stack_topLevels_MoveTo ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
var valueToMove = new Window { Id = "w1" };
var comparer = new ToplevelEqualityComparer ();
Toplevels.MoveTo (valueToMove, 1, comparer);
topLevels.MoveTo (valueToMove, 1, comparer);
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("w4", tops [0].Id);
Assert.Equal ("w1", tops [1].Id);
@@ -109,16 +111,16 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
}
[Fact]
public void Stack_Toplevels_MoveTo_From_Last_To_Top ()
public void Stack_topLevels_MoveTo_From_Last_To_Top ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
var valueToMove = new Window { Id = "Top" };
var comparer = new ToplevelEqualityComparer ();
Toplevels.MoveTo (valueToMove, 0, comparer);
topLevels.MoveTo (valueToMove, 0, comparer);
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("Top", tops [0].Id);
Assert.Equal ("w4", tops [1].Id);
@@ -128,17 +130,17 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
}
[Fact]
public void Stack_Toplevels_Replace ()
public void Stack_topLevels_Replace ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
var valueToReplace = new Window { Id = "w1" };
var valueToReplaceWith = new Window { Id = "new" };
var comparer = new ToplevelEqualityComparer ();
Toplevels.Replace (valueToReplace, valueToReplaceWith, comparer);
topLevels.Replace (valueToReplace, valueToReplaceWith, comparer);
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("w4", tops [0].Id);
Assert.Equal ("w3", tops [1].Id);
@@ -148,16 +150,16 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
}
[Fact]
public void Stack_Toplevels_Swap ()
public void Stack_topLevels_Swap ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
var valueToSwapFrom = new Window { Id = "w3" };
var valueToSwapTo = new Window { Id = "w1" };
var comparer = new ToplevelEqualityComparer ();
Toplevels.Swap (valueToSwapFrom, valueToSwapTo, comparer);
topLevels.Swap (valueToSwapFrom, valueToSwapTo, comparer);
Toplevel [] tops = Toplevels.ToArray ();
Toplevel [] tops = topLevels.ToArray ();
Assert.Equal ("w4", tops [0].Id);
Assert.Equal ("w1", tops [1].Id);
@@ -169,27 +171,27 @@ public class StackExtensionsTests : UnitTests.Parallelizable.ParallelizableBase
[Fact]
public void ToplevelEqualityComparer_GetHashCode ()
{
Stack<Toplevel> Toplevels = CreateToplevels ();
Stack<Toplevel> topLevels = CreatetopLevels ();
// Only allows unique keys
HashSet<int> hCodes = new ();
foreach (Toplevel top in Toplevels)
foreach (Toplevel top in topLevels)
{
Assert.True (hCodes.Add (top.GetHashCode ()));
}
}
private Stack<Toplevel> CreateToplevels ()
private Stack<Toplevel> CreatetopLevels ()
{
Stack<Toplevel> Toplevels = new ();
Stack<Toplevel> topLevels = new ();
Toplevels.Push (new Toplevel { Id = "Top" });
Toplevels.Push (new Window { Id = "w1" });
Toplevels.Push (new Window { Id = "w2" });
Toplevels.Push (new Window { Id = "w3" });
Toplevels.Push (new Window { Id = "w4" });
topLevels.Push (new Toplevel { Id = "Top" });
topLevels.Push (new Window { Id = "w1" });
topLevels.Push (new Window { Id = "w2" });
topLevels.Push (new Window { Id = "w3" });
topLevels.Push (new Window { Id = "w4" });
return Toplevels;
return topLevels;
}
}

View File

@@ -226,7 +226,9 @@ public class ScopeTests
}));
}
#pragma warning disable xUnit1031
Task.WaitAll (tasks.ToArray ());
#pragma warning restore xUnit1031
// Assert
Assert.Equal (threadCount * itemsPerThread, scope.Count);

View File

@@ -1,8 +1,10 @@
// Alias Console to MockConsole so we don't accidentally use Console
using UnitTests;
namespace UnitTests_Parallelizable.DrawingTests;
public class AttributeTests
public class AttributeTests : FakeDriverBase
{
[Fact]
public void Constructor_ParsesNamedColorsAndStyle ()
@@ -36,7 +38,7 @@ public class AttributeTests
public void Constructor_DefaultsToNoneStyle_WhenStyleIsNullOrEmpty ()
{
var attr1 = new Attribute ("White", "Black");
var attr2 = new Attribute ("White", "Black", null);
var attr2 = new Attribute ("White", "Black");
var attr3 = new Attribute ("White", "Black", "");
Assert.Equal (TextStyle.None, attr1.Style);
Assert.Equal (TextStyle.None, attr2.Style);
@@ -103,8 +105,7 @@ public class AttributeTests
[Fact]
public void Constructors_Construct ()
{
var driver = new FakeDriver ();
driver.Init ();
IDriver driver = CreateFakeDriver ();
// Test parameterless constructor
var attr = new Attribute ();
@@ -258,8 +259,7 @@ public class AttributeTests
[Fact]
public void Make_Creates ()
{
var driver = new FakeDriver ();
driver.Init ();
IDriver driver = CreateFakeDriver ();
var fg = new Color ();
fg = new (Color.Red);

View File

@@ -1,6 +1,5 @@
using System.Text;
using UnitTests;
using UnitTests.Parallelizable;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DrawingTests;
@@ -11,7 +10,7 @@ namespace UnitTests_Parallelizable.DrawingTests;
/// Note: Tests that verify rendered output (ToString()) cannot be parallelized because LineCanvas
/// depends on Application.Driver for glyph resolution and configuration. Those tests remain in UnitTests.
/// </summary>
public class LineCanvasTests (ITestOutputHelper output) : ParallelizableBase
public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase
{
#region Basic API Tests
@@ -548,7 +547,7 @@ public class LineCanvasTests (ITestOutputHelper output) : ParallelizableBase
string expected
)
{
IConsoleDriver driver = CreateFakeDriver ();
IDriver driver = CreateFakeDriver ();
View v = GetCanvas (driver, out LineCanvas lc);
v.Width = 10;
v.Height = 10;
@@ -1483,7 +1482,7 @@ public class LineCanvasTests (ITestOutputHelper output) : ParallelizableBase
/// <param name="offsetX">How far to offset drawing in X</param>
/// <param name="offsetY">How far to offset drawing in Y</param>
/// <returns></returns>
private View GetCanvas (IConsoleDriver driver, out LineCanvas canvas, int offsetX = 0, int offsetY = 0)
private View GetCanvas (IDriver driver, out LineCanvas canvas, int offsetX = 0, int offsetY = 0)
{
var v = new View { Width = 10, Height = 5, Viewport = new (0, 0, 10, 5) };
v.Driver = driver;

View File

@@ -1,6 +1,5 @@
using Microsoft.VisualStudio.TestPlatform.Utilities;
using UnitTests;
using UnitTests.Parallelizable;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DrawingTests;
@@ -11,7 +10,7 @@ namespace UnitTests_Parallelizable.DrawingTests;
///
/// Note: Tests that verify rendered output (Draw methods) require Application.Driver and remain in UnitTests as integration tests.
/// </summary>
public class RulerTests (ITestOutputHelper output): ParallelizableBase
public class RulerTests (ITestOutputHelper output) : FakeDriverBase
{
[Fact]
public void Constructor_Defaults ()
@@ -52,7 +51,7 @@ public class RulerTests (ITestOutputHelper output): ParallelizableBase
[Fact]
public void Draw_Default ()
{
IConsoleDriver driver = CreateFakeDriver ();
IDriver driver = CreateFakeDriver ();
var r = new Ruler ();
r.Draw (Point.Empty, driver: driver);
@@ -62,7 +61,7 @@ public class RulerTests (ITestOutputHelper output): ParallelizableBase
[Fact]
public void Draw_Horizontal ()
{
IConsoleDriver driver = CreateFakeDriver ();
IDriver driver = CreateFakeDriver ();
var len = 15;
@@ -108,7 +107,7 @@ public class RulerTests (ITestOutputHelper output): ParallelizableBase
[Fact]
public void Draw_Vertical ()
{
IConsoleDriver driver = CreateFakeDriver ();
IDriver driver = CreateFakeDriver ();
var len = 15;

View File

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

View File

@@ -1,11 +1,10 @@
using System.Text;
using UnitTests;
using UnitTests.Parallelizable;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DrawingTests;
public class ThicknessTests (ITestOutputHelper output) : ParallelizableBase
public class ThicknessTests (ITestOutputHelper output) : FakeDriverBase
{
[Fact]
public void Constructor_Defaults ()
@@ -625,7 +624,7 @@ public class ThicknessTests (ITestOutputHelper output) : ParallelizableBase
[Fact]
public void DrawTests ()
{
IConsoleDriver driver = new FakeDriver ();
IDriver driver = CreateFakeDriver ();
driver.SetScreenSize (60, 40);
var t = new Thickness (0, 0, 0, 0);
@@ -738,7 +737,7 @@ public class ThicknessTests (ITestOutputHelper output) : ParallelizableBase
[Fact]
public void DrawTests_Ruler ()
{
IConsoleDriver driver = new FakeDriver ();
IDriver driver = CreateFakeDriver ();
// Add a frame so we can see the ruler
var f = new FrameView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single };

View File

@@ -0,0 +1,180 @@
using System.Buffers;
using System.Text;
using UnitTests;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests_Parallelizable.DriverTests;
public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void AddRune ()
{
IDriver driver = CreateFakeDriver ();
driver.Rows = 25;
driver.Cols = 80;
driver.AddRune (new Rune ('a'));
Assert.Equal ((Rune)'a', driver.Contents [0, 0].Rune);
driver.End ();
}
[Fact]
public void AddRune_Accented_Letter_With_Three_Combining_Unicode_Chars ()
{
IDriver driver = CreateFakeDriver ();
var expected = new Rune ('ắ');
var text = "\u1eaf";
driver.AddStr (text);
Assert.Equal (expected, driver.Contents [0, 0].Rune);
Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune);
driver.ClearContents ();
driver.Move (0, 0);
text = "\u0103\u0301";
driver.AddStr (text);
Assert.Equal (expected, driver.Contents [0, 0].Rune);
Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune);
driver.ClearContents ();
driver.Move (0, 0);
text = "\u0061\u0306\u0301";
driver.AddStr (text);
Assert.Equal (expected, driver.Contents [0, 0].Rune);
Assert.Equal ((Rune)' ', driver.Contents [0, 1].Rune);
// var s = "a\u0301\u0300\u0306";
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
//ắ", output);
// tf.Text = "\u1eaf";
// Application.Refresh ();
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
//ắ", output);
// tf.Text = "\u0103\u0301";
// Application.Refresh ();
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
//ắ", output);
// tf.Text = "\u0061\u0306\u0301";
// Application.Refresh ();
// DriverAsserts.AssertDriverContentsWithFrameAre (@"
//ắ", output);
driver.End ();
}
[Fact]
public void AddRune_InvalidLocation_DoesNothing ()
{
IDriver driver = CreateFakeDriver ();
driver.Move (driver.Cols, driver.Rows);
driver.AddRune ('a');
for (var col = 0; col < driver.Cols; col++)
{
for (var row = 0; row < driver.Rows; row++)
{
Assert.Equal ((Rune)' ', driver.Contents [row, col].Rune);
}
}
driver.End ();
}
[Fact]
public void AddRune_MovesToNextColumn ()
{
IDriver driver = CreateFakeDriver ();
driver.AddRune ('a');
Assert.Equal ((Rune)'a', driver.Contents [0, 0].Rune);
Assert.Equal (0, driver.Row);
Assert.Equal (1, driver.Col);
driver.AddRune ('b');
Assert.Equal ((Rune)'b', driver.Contents [0, 1].Rune);
Assert.Equal (0, driver.Row);
Assert.Equal (2, driver.Col);
// Move to the last column of the first row
int lastCol = driver.Cols - 1;
driver.Move (lastCol, 0);
Assert.Equal (0, driver.Row);
Assert.Equal (lastCol, driver.Col);
// Add a rune to the last column of the first row; should increment the row or col even though it's now invalid
driver.AddRune ('c');
Assert.Equal ((Rune)'c', driver.Contents [0, lastCol].Rune);
Assert.Equal (lastCol + 1, driver.Col);
// Add a rune; should succeed but do nothing as it's outside of Contents
driver.AddRune ('d');
Assert.Equal (lastCol + 2, driver.Col);
for (var col = 0; col < driver.Cols; col++)
{
for (var row = 0; row < driver.Rows; row++)
{
Assert.NotEqual ((Rune)'d', driver.Contents [row, col].Rune);
}
}
driver.End ();
}
[Fact]
public void AddRune_MovesToNextColumn_Wide ()
{
IDriver driver = CreateFakeDriver ();
// 🍕 Slice of Pizza "\U0001F355"
OperationStatus operationStatus = Rune.DecodeFromUtf16 ("\U0001F355", out Rune rune, out int charsConsumed);
Assert.Equal (OperationStatus.Done, operationStatus);
Assert.Equal (charsConsumed, rune.Utf16SequenceLength);
Assert.Equal (2, rune.GetColumns ());
driver.AddRune (rune);
Assert.Equal (rune, driver.Contents [0, 0].Rune);
Assert.Equal (0, driver.Row);
Assert.Equal (2, driver.Col);
//driver.AddRune ('b');
//Assert.Equal ((Rune)'b', driver.Contents [0, 1].Rune);
//Assert.Equal (0, driver.Row);
//Assert.Equal (2, driver.Col);
//// Move to the last column of the first row
//var lastCol = driver.Cols - 1;
//driver.Move (lastCol, 0);
//Assert.Equal (0, driver.Row);
//Assert.Equal (lastCol, driver.Col);
//// Add a rune to the last column of the first row; should increment the row or col even though it's now invalid
//driver.AddRune ('c');
//Assert.Equal ((Rune)'c', driver.Contents [0, lastCol].Rune);
//Assert.Equal (lastCol + 1, driver.Col);
//// Add a rune; should succeed but do nothing as it's outside of Contents
//driver.AddRune ('d');
//Assert.Equal (lastCol + 2, driver.Col);
//for (var col = 0; col < driver.Cols; col++) {
// for (var row = 0; row < driver.Rows; row++) {
// Assert.NotEqual ((Rune)'d', driver.Contents [row, col].Rune);
// }
//}
driver.End ();
}
}

View File

@@ -0,0 +1,130 @@
#nullable enable
namespace UnitTests_Parallelizable.DriverTests;
public class AnsiKeyboardParserTests
{
private readonly AnsiKeyboardParser _parser = new ();
public static IEnumerable<object? []> GetKeyboardTestData ()
{
// Test data for various ANSI escape sequences and their expected Key values
yield return ["\u001b[A", Key.CursorUp];
yield return ["\u001b[B", Key.CursorDown];
yield return ["\u001b[C", Key.CursorRight];
yield return ["\u001b[D", Key.CursorLeft];
// Valid inputs with modifiers
yield return ["\u001b[1;2A", Key.CursorUp.WithShift];
yield return ["\u001b[1;3A", Key.CursorUp.WithAlt];
yield return ["\u001b[1;4A", Key.CursorUp.WithAlt.WithShift];
yield return ["\u001b[1;5A", Key.CursorUp.WithCtrl];
yield return ["\u001b[1;6A", Key.CursorUp.WithCtrl.WithShift];
yield return ["\u001b[1;7A", Key.CursorUp.WithCtrl.WithAlt];
yield return ["\u001b[1;8A", Key.CursorUp.WithCtrl.WithAlt.WithShift];
yield return ["\u001b[1;2B", Key.CursorDown.WithShift];
yield return ["\u001b[1;3B", Key.CursorDown.WithAlt];
yield return ["\u001b[1;4B", Key.CursorDown.WithAlt.WithShift];
yield return ["\u001b[1;5B", Key.CursorDown.WithCtrl];
yield return ["\u001b[1;6B", Key.CursorDown.WithCtrl.WithShift];
yield return ["\u001b[1;7B", Key.CursorDown.WithCtrl.WithAlt];
yield return ["\u001b[1;8B", Key.CursorDown.WithCtrl.WithAlt.WithShift];
yield return ["\u001b[1;2C", Key.CursorRight.WithShift];
yield return ["\u001b[1;3C", Key.CursorRight.WithAlt];
yield return ["\u001b[1;4C", Key.CursorRight.WithAlt.WithShift];
yield return ["\u001b[1;5C", Key.CursorRight.WithCtrl];
yield return ["\u001b[1;6C", Key.CursorRight.WithCtrl.WithShift];
yield return ["\u001b[1;7C", Key.CursorRight.WithCtrl.WithAlt];
yield return ["\u001b[1;8C", Key.CursorRight.WithCtrl.WithAlt.WithShift];
yield return ["\u001b[1;2D", Key.CursorLeft.WithShift];
yield return ["\u001b[1;3D", Key.CursorLeft.WithAlt];
yield return ["\u001b[1;4D", Key.CursorLeft.WithAlt.WithShift];
yield return ["\u001b[1;5D", Key.CursorLeft.WithCtrl];
yield return ["\u001b[1;6D", Key.CursorLeft.WithCtrl.WithShift];
yield return ["\u001b[1;7D", Key.CursorLeft.WithCtrl.WithAlt];
yield return ["\u001b[1;8D", Key.CursorLeft.WithCtrl.WithAlt.WithShift];
// Invalid inputs
yield return ["\u001b[Z", null!];
yield return ["\u001b[invalid", null!];
yield return ["\u001b[1", null!];
yield return ["\u001b[AB", null!];
yield return ["\u001b[;A", null!];
// Test data for various ANSI escape sequences and their expected Key values
yield return ["\u001b[3;5~", Key.Delete.WithCtrl];
// Additional special keys
yield return ["\u001b[H", Key.Home];
yield return ["\u001b[F", Key.End];
yield return ["\u001b[2~", Key.InsertChar];
yield return ["\u001b[5~", Key.PageUp];
yield return ["\u001b[6~", Key.PageDown];
// Home, End with modifiers
yield return ["\u001b[1;2H", Key.Home.WithShift];
yield return ["\u001b[1;3H", Key.Home.WithAlt];
yield return ["\u001b[1;5F", Key.End.WithCtrl];
// Insert with modifiers
yield return ["\u001b[2;2~", Key.InsertChar.WithShift];
yield return ["\u001b[2;3~", Key.InsertChar.WithAlt];
yield return ["\u001b[2;5~", Key.InsertChar.WithCtrl];
// PageUp/PageDown with modifiers
yield return ["\u001b[5;2~", Key.PageUp.WithShift];
yield return ["\u001b[6;3~", Key.PageDown.WithAlt];
yield return ["\u001b[6;5~", Key.PageDown.WithCtrl];
// Function keys F1-F4 (common ESC O sequences)
yield return ["\u001bOP", Key.F1];
yield return ["\u001bOQ", Key.F2];
yield return ["\u001bOR", Key.F3];
yield return ["\u001bOS", Key.F4];
// Extended function keys F1-F12 with CSI sequences
yield return ["\u001b[11~", Key.F1];
yield return ["\u001b[12~", Key.F2];
yield return ["\u001b[13~", Key.F3];
yield return ["\u001b[14~", Key.F4];
yield return ["\u001b[15~", Key.F5];
yield return ["\u001b[17~", Key.F6];
yield return ["\u001b[18~", Key.F7];
yield return ["\u001b[19~", Key.F8];
yield return ["\u001b[20~", Key.F9];
yield return ["\u001b[21~", Key.F10];
yield return ["\u001b[23~", Key.F11];
yield return ["\u001b[24~", Key.F12];
// Function keys with modifiers
yield return ["\u001b[1;2P", Key.F1.WithShift];
yield return ["\u001b[1;3Q", Key.F2.WithAlt];
yield return ["\u001b[1;5R", Key.F3.WithCtrl];
// Keys with Alt modifiers
yield return ["\u001ba", Key.A.WithAlt, true];
yield return ["\u001bA", Key.A.WithShift.WithAlt, true];
yield return ["\u001b1", Key.D1.WithAlt, true];
// Keys with Ctrl and Alt modifiers
yield return ["\u001b\u0001", Key.A.WithCtrl.WithAlt, true];
yield return ["\u001b\u001a", Key.Z.WithCtrl.WithAlt, true];
// Keys with Ctrl, Shift and Alt modifiers
yield return ["\u001b\u001f", Key.D7.WithCtrl.WithShift.WithAlt, true];
}
// Consolidated test for all keyboard events (e.g., arrow keys)
[Theory]
[MemberData (nameof (GetKeyboardTestData))]
public void ProcessKeyboardInput_ReturnsCorrectKey (string? input, Key? expectedKey, bool isLastMinute = false)
{
// Act
Key? result = _parser.IsKeyboard (input, isLastMinute)?.GetKey (input);
// Assert
Assert.Equal (expectedKey, result); // Verify the returned key matches the expected one
}
}

View File

@@ -0,0 +1,40 @@
namespace UnitTests_Parallelizable.DriverTests;
public class AnsiMouseParserTests
{
private readonly AnsiMouseParser _parser = new ();
// Consolidated test for all mouse events: button press/release, wheel scroll, position, modifiers
[Theory]
[InlineData ("\u001b[<0;100;200M", 99, 199, MouseFlags.Button1Pressed)] // Button 1 Pressed
[InlineData ("\u001b[<0;150;250m", 149, 249, MouseFlags.Button1Released)] // Button 1 Released
[InlineData ("\u001b[<1;120;220M", 119, 219, MouseFlags.Button2Pressed)] // Button 2 Pressed
[InlineData ("\u001b[<1;180;280m", 179, 279, MouseFlags.Button2Released)] // Button 2 Released
[InlineData ("\u001b[<2;200;300M", 199, 299, MouseFlags.Button3Pressed)] // Button 3 Pressed
[InlineData ("\u001b[<2;250;350m", 249, 349, MouseFlags.Button3Released)] // Button 3 Released
[InlineData ("\u001b[<64;100;200M", 99, 199, MouseFlags.WheeledUp)] // Wheel Scroll Up
[InlineData ("\u001b[<65;150;250m", 149, 249, MouseFlags.WheeledDown)] // Wheel Scroll Down
[InlineData ("\u001b[<39;100;200m", 99, 199, MouseFlags.ButtonShift | MouseFlags.ReportMousePosition)] // Mouse Position (No Button)
[InlineData ("\u001b[<43;120;240m", 119, 239, MouseFlags.ButtonAlt | MouseFlags.ReportMousePosition)] // Mouse Position (No Button)
[InlineData ("\u001b[<8;100;200M", 99, 199, MouseFlags.Button1Pressed | MouseFlags.ButtonAlt)] // Button 1 Pressed + Alt
[InlineData ("\u001b[<invalid;100;200M", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null)
[InlineData ("\u001b[<100;200;300Z", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null)
[InlineData ("\u001b[<invalidInput>", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null)
public void ProcessMouseInput_ReturnsCorrectFlags (string input, int expectedX, int expectedY, MouseFlags expectedFlags)
{
// Act
MouseEventArgs result = _parser.ProcessMouseInput (input);
// Assert
if (expectedFlags == MouseFlags.None)
{
Assert.Null (result); // Expect null for invalid inputs
}
else
{
Assert.NotNull (result); // Expect non-null result for valid inputs
Assert.Equal (new (expectedX, expectedY), result!.Position); // Verify position
Assert.Equal (expectedFlags, result.Flags); // Verify flags
}
}
}

View File

@@ -0,0 +1,232 @@
using Moq;
namespace UnitTests_Parallelizable.DriverTests;
public class AnsiRequestSchedulerTests
{
private readonly Mock<IAnsiResponseParser> _parserMock;
private readonly AnsiRequestScheduler _scheduler;
private static DateTime _staticNow; // Static value to hold the current time
public AnsiRequestSchedulerTests ()
{
_parserMock = new (MockBehavior.Strict);
_staticNow = DateTime.UtcNow; // Initialize static time
_scheduler = new (_parserMock.Object, () => _staticNow);
}
[Fact]
public void SendOrSchedule_SendsDeviceAttributeRequest_WhenNoOutstandingRequests ()
{
// Arrange
var request = new AnsiEscapeSequenceRequest
{
Request = "\u001b[0c", // ESC [ c
Terminator = "c",
ResponseReceived = r => { }
};
// we have no outstanding for c already
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once);
// then we should execute our request
_parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny<Action<string>> (), null, false)).Verifiable (Times.Once);
// Act
bool result = _scheduler.SendOrSchedule (request);
// Assert
Assert.Empty (_scheduler.QueuedRequests); // We sent it i.e. we did not queue it for later
Assert.True (result); // Should send immediately
_parserMock.Verify ();
}
[Fact]
public void SendOrSchedule_QueuesRequest_WhenOutstandingRequestExists ()
{
// Arrange
var request1 = new AnsiEscapeSequenceRequest
{
Request = "\u001b[0c", // ESC [ 0 c
Terminator = "c",
ResponseReceived = r => { }
};
// Parser already has an ongoing request for "c"
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Once);
// Act
bool result = _scheduler.SendOrSchedule (request1);
// Assert
Assert.Single (_scheduler.QueuedRequests); // Ensure only one request is in the queue
Assert.False (result); // Should be queued
_parserMock.Verify ();
}
[Fact]
public void RunSchedule_ThrottleNotExceeded_AllowSend ()
{
// Arrange
var request = new AnsiEscapeSequenceRequest
{
Request = "\u001b[0c", // ESC [ 0 c
Terminator = "c",
ResponseReceived = r => { }
};
// Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2));
_parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny<Action<string>> (), null, false)).Verifiable (Times.Exactly (2));
_scheduler.SendOrSchedule (request);
// Simulate time passing beyond throttle
SetTime (101); // Exceed throttle limit
// Act
// Send another request after the throttled time limit
bool result = _scheduler.SendOrSchedule (request);
// Assert
Assert.Empty (_scheduler.QueuedRequests); // Should send and clear the request
Assert.True (result); // Should have found and sent the request
_parserMock.Verify ();
}
[Fact]
public void RunSchedule_ThrottleExceeded_QueueRequest ()
{
// Arrange
var request = new AnsiEscapeSequenceRequest
{
Request = "\u001b[0c", // ESC [ 0 c
Terminator = "c",
ResponseReceived = r => { }
};
// Set up to expect no outstanding request for "c" i.e. parser instantly gets response and resolves it
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Exactly (2));
_parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny<Action<string>> (), null, false)).Verifiable (Times.Exactly (2));
_scheduler.SendOrSchedule (request);
// Simulate time passing
SetTime (55); // Does not exceed throttle limit
// Act
// Send another request after the throttled time limit
bool result = _scheduler.SendOrSchedule (request);
// Assert
Assert.Single (_scheduler.QueuedRequests); // Should have been queued
Assert.False (result); // Should have been queued
// Throttle still not exceeded
Assert.False (_scheduler.RunSchedule ());
SetTime (90);
// Throttle still not exceeded
Assert.False (_scheduler.RunSchedule ());
SetTime (105);
// Throttle exceeded - so send the request
Assert.True (_scheduler.RunSchedule ());
_parserMock.Verify ();
}
[Fact]
public void EvictStaleRequests_RemovesStaleRequest_AfterTimeout ()
{
// Arrange
var request1 = new AnsiEscapeSequenceRequest
{
Request = "\u001b[0c",
Terminator = "c",
ResponseReceived = r => { }
};
// Send
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once);
_parserMock.Setup (p => p.ExpectResponse ("c", It.IsAny<Action<string>> (), null, false)).Verifiable (Times.Exactly (2));
Assert.True (_scheduler.SendOrSchedule (request1));
// Parser already has an ongoing request for "c"
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Exactly (2));
// Cannot send because there is already outstanding request
Assert.False (_scheduler.SendOrSchedule (request1));
Assert.Single (_scheduler.QueuedRequests);
// Simulate request going stale
SetTime (5001); // Exceeds stale timeout
// Parser should be told to give up on this one (evicted)
_parserMock.Setup (p => p.StopExpecting ("c", false))
.Callback (() =>
{
// When we tell parser to evict - it should now tell us it is no longer expecting
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (false).Verifiable (Times.Once);
})
.Verifiable ();
// When we send again the evicted one should be
bool evicted = _scheduler.RunSchedule ();
Assert.True (evicted); // Stale request should be evicted
Assert.Empty (_scheduler.QueuedRequests);
// Assert
_parserMock.Verify ();
}
[Fact]
public void RunSchedule_DoesNothing_WhenQueueIsEmpty ()
{
// Act
bool result = _scheduler.RunSchedule ();
// Assert
Assert.False (result); // No requests to process
Assert.Empty (_scheduler.QueuedRequests);
}
[Fact]
public void SendOrSchedule_ManagesIndependentTerminatorsCorrectly ()
{
// Arrange
var request1 = new AnsiEscapeSequenceRequest { Request = "\u001b[0c", Terminator = "c", ResponseReceived = r => { } };
var request2 = new AnsiEscapeSequenceRequest { Request = "\u001b[0x", Terminator = "x", ResponseReceived = r => { } };
// Already have a 'c' ongoing
_parserMock.Setup (p => p.IsExpecting ("c")).Returns (true).Verifiable (Times.Once);
// 'x' is free
_parserMock.Setup (p => p.IsExpecting ("x")).Returns (false).Verifiable (Times.Once);
_parserMock.Setup (p => p.ExpectResponse ("x", It.IsAny<Action<string>> (), null, false)).Verifiable (Times.Once);
// Act
bool a = _scheduler.SendOrSchedule (request1);
bool b = _scheduler.SendOrSchedule (request2);
// Assert
Assert.False (a);
Assert.True (b);
Assert.Equal (request1, Assert.Single (_scheduler.QueuedRequests));
_parserMock.Verify ();
}
private void SetTime (int milliseconds)
{
// This simulates the passing of time by setting the Now function to return a specific time.
DateTime newNow = _staticNow.AddMilliseconds (milliseconds);
_scheduler.Now = () => newNow;
}
}

View File

@@ -5,6 +5,7 @@ using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
// BUGBUG: These tests use TInputRecord of `int`, but that's not a realistic type for keyboard input.
public class AnsiResponseParserTests (ITestOutputHelper output)
{
private readonly AnsiResponseParser<int> _parser1 = new ();

View File

@@ -0,0 +1,422 @@
namespace UnitTests_Parallelizable.DriverTests;
public class ConsoleKeyMappingTests
{
[Theory]
[InlineData ('a', ConsoleKey.A, false, false, false, (KeyCode)'a')]
[InlineData ('A', ConsoleKey.A, true, false, false, KeyCode.A | KeyCode.ShiftMask)]
[InlineData ('á', ConsoleKey.A, false, false, false, (KeyCode)'á')]
[InlineData ('Á', ConsoleKey.A, true, false, false, (KeyCode)'Á' | KeyCode.ShiftMask)]
[InlineData ('à', ConsoleKey.A, false, false, false, (KeyCode)'à')]
[InlineData ('À', ConsoleKey.A, true, false, false, (KeyCode)'À' | KeyCode.ShiftMask)]
[InlineData ('5', ConsoleKey.D5, false, false, false, KeyCode.D5)]
[InlineData ('%', ConsoleKey.D5, true, false, false, (KeyCode)'%' | KeyCode.ShiftMask)]
[InlineData ('€', ConsoleKey.D5, false, true, true, (KeyCode)'€' | KeyCode.AltMask | KeyCode.CtrlMask)]
[InlineData ('?', ConsoleKey.Oem4, true, false, false, (KeyCode)'?' | KeyCode.ShiftMask)]
[InlineData ('\'', ConsoleKey.Oem4, false, false, false, (KeyCode)'\'')]
[InlineData ('q', ConsoleKey.Q, false, false, false, (KeyCode)'q')]
[InlineData ('\0', ConsoleKey.F2, false, false, false, KeyCode.F2)]
[InlineData ('英', ConsoleKey.None, false, false, false, (KeyCode)'英')]
[InlineData ('\r', ConsoleKey.Enter, false, false, false, KeyCode.Enter)]
public void MapConsoleKeyInfoToKeyCode_Also_Return_Modifiers (
char keyChar,
ConsoleKey consoleKey,
bool shift,
bool alt,
bool control,
KeyCode expectedKeyCode
)
{
var consoleKeyInfo = new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control);
KeyCode keyCode = ConsoleKeyMapping.MapConsoleKeyInfoToKeyCode (consoleKeyInfo);
Assert.Equal (keyCode, expectedKeyCode);
}
[Theory]
[InlineData ('a', false, false, false, (KeyCode)'a')]
[InlineData ('A', true, false, false, KeyCode.A | KeyCode.ShiftMask)]
[InlineData ('á', false, false, false, (KeyCode)'á')]
[InlineData ('Á', true, false, false, (KeyCode)'Á' | KeyCode.ShiftMask)]
[InlineData ('à', false, false, false, (KeyCode)'à')]
[InlineData ('À', true, false, false, (KeyCode)'À' | KeyCode.ShiftMask)]
[InlineData ('5', false, false, false, KeyCode.D5)]
[InlineData ('%', true, false, false, (KeyCode)'%' | KeyCode.ShiftMask)]
[InlineData ('€', false, true, true, (KeyCode)'€' | KeyCode.AltMask | KeyCode.CtrlMask)]
[InlineData ('?', true, false, false, (KeyCode)'?' | KeyCode.ShiftMask)]
[InlineData ('\'', false, false, false, (KeyCode)'\'')]
[InlineData ('q', false, false, false, (KeyCode)'q')]
[InlineData ((uint)KeyCode.F2, false, false, false, KeyCode.F2)]
[InlineData ('英', false, false, false, (KeyCode)'英')]
[InlineData ('\r', false, false, false, KeyCode.Enter)]
[InlineData ('\n', false, false, false, (KeyCode)'\n')]
public void MapToKeyCodeModifiers_Tests (
uint keyChar,
bool shift,
bool alt,
bool control,
KeyCode expectedKeyCode
)
{
ConsoleModifiers modifiers = ConsoleKeyMapping.GetModifiers (shift, alt, control);
var keyCode = (KeyCode)keyChar;
keyCode = ConsoleKeyMapping.MapToKeyCodeModifiers (modifiers, keyCode);
Assert.Equal (keyCode, expectedKeyCode);
}
public static IEnumerable<object []> UnShiftedChars =>
new List<object []>
{
new object [] { 'a', 'A', KeyCode.A | KeyCode.ShiftMask },
new object [] { 'z', 'Z', KeyCode.Z | KeyCode.ShiftMask },
new object [] { 'á', 'Á', (KeyCode)'Á' | KeyCode.ShiftMask },
new object [] { 'à', 'À', (KeyCode)'À' | KeyCode.ShiftMask },
new object [] { 'ý', 'Ý', (KeyCode)'Ý' | KeyCode.ShiftMask },
new object [] { '1', '!', (KeyCode)'!' | KeyCode.ShiftMask },
new object [] { '2', '"', (KeyCode)'"' | KeyCode.ShiftMask },
new object [] { '3', '#', (KeyCode)'#' | KeyCode.ShiftMask },
new object [] { '4', '$', (KeyCode)'$' | KeyCode.ShiftMask },
new object [] { '5', '%', (KeyCode)'%' | KeyCode.ShiftMask },
new object [] { '6', '&', (KeyCode)'&' | KeyCode.ShiftMask },
new object [] { '7', '/', (KeyCode)'/' | KeyCode.ShiftMask },
new object [] { '8', '(', (KeyCode)'(' | KeyCode.ShiftMask },
new object [] { '9', ')', (KeyCode)')' | KeyCode.ShiftMask },
new object [] { '0', '=', (KeyCode)'=' | KeyCode.ShiftMask },
new object [] { '\\', '|', (KeyCode)'|' | KeyCode.ShiftMask },
new object [] { '\'', '?', (KeyCode)'?' | KeyCode.ShiftMask },
new object [] { '«', '»', (KeyCode)'»' | KeyCode.ShiftMask },
new object [] { '+', '*', (KeyCode)'*' | KeyCode.ShiftMask },
new object [] { '´', '`', (KeyCode)'`' | KeyCode.ShiftMask },
new object [] { 'º', 'ª', (KeyCode)'ª' | KeyCode.ShiftMask },
new object [] { '~', '^', (KeyCode)'^' | KeyCode.ShiftMask },
new object [] { '<', '>', (KeyCode)'>' | KeyCode.ShiftMask },
new object [] { ',', ';', (KeyCode)';' | KeyCode.ShiftMask },
new object [] { '.', ':', (KeyCode)':' | KeyCode.ShiftMask },
new object [] { '-', '_', (KeyCode)'_' | KeyCode.ShiftMask }
};
[Theory]
[InlineData (KeyCode.A, false, false, false)] // Unshifted A (lowercase)
[InlineData (KeyCode.B, false, false, false)]
[InlineData (KeyCode.Z, false, false, false)]
[InlineData (KeyCode.A | KeyCode.ShiftMask, true, false, false)] // Shifted A (uppercase)
[InlineData (KeyCode.Z | KeyCode.ShiftMask, true, false, false)]
[InlineData (KeyCode.A | KeyCode.CtrlMask, false, false, true)] // Ctrl+A
[InlineData (KeyCode.A | KeyCode.AltMask, false, true, false)] // Alt+A
[InlineData (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask, true, false, true)] // Ctrl+Shift+A
[InlineData (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask, true, true, false)] // Alt+Shift+A
[InlineData (KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask, false, true, true)] // Ctrl+Alt+A
[InlineData (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask, true, true, true)] // All modifiers
public void MapToConsoleModifiers_LetterKeys_ReturnsCorrectModifiers (
KeyCode key,
bool expectedShift,
bool expectedAlt,
bool expectedControl
)
{
// Act
ConsoleModifiers result = ConsoleKeyMapping.MapToConsoleModifiers (key);
// Assert
Assert.Equal (expectedShift, result.HasFlag (ConsoleModifiers.Shift));
Assert.Equal (expectedAlt, result.HasFlag (ConsoleModifiers.Alt));
Assert.Equal (expectedControl, result.HasFlag (ConsoleModifiers.Control));
}
[Theory]
[InlineData (KeyCode.A)] // 65 = 'A' in ASCII, but represents unshifted key
[InlineData (KeyCode.B)]
[InlineData (KeyCode.M)]
[InlineData (KeyCode.Z)]
public void MapToConsoleModifiers_UnshiftedLetterKeys_DoesNotSetShiftFlag (KeyCode key)
{
// This test verifies the BUGFIX: KeyCode.A-Z (65-90) represent UNSHIFTED keys,
// even though their numeric values match uppercase ASCII characters.
// The old code incorrectly checked char.IsUpper((char)key) which would fail this test.
// Act
ConsoleModifiers result = ConsoleKeyMapping.MapToConsoleModifiers (key);
// Assert - Shift should NOT be set for unshifted letter keys
Assert.False (result.HasFlag (ConsoleModifiers.Shift),
$"Shift should not be set for unshifted {key}. The KeyCode value {(int)key} represents a lowercase, unshifted key.");
}
[Theory]
[InlineData (KeyCode.D1, false)] // Unshifted number keys
[InlineData (KeyCode.D5, false)]
[InlineData (KeyCode.Space, false)]
[InlineData (KeyCode.Enter, false)]
[InlineData (KeyCode.Tab, false)]
[InlineData (KeyCode.D1 | KeyCode.ShiftMask, true)] // Shifted number keys
public void MapToConsoleModifiers_NonLetterKeys_ReturnsCorrectShiftState (KeyCode key, bool expectedShift)
{
// Act
ConsoleModifiers result = ConsoleKeyMapping.MapToConsoleModifiers (key);
// Assert
Assert.Equal (expectedShift, result.HasFlag (ConsoleModifiers.Shift));
}
[Theory]
[InlineData (KeyCode.A, 'a', ConsoleKey.A, false)] // Unshifted A = lowercase 'a'
[InlineData (KeyCode.B, 'b', ConsoleKey.B, false)]
[InlineData (KeyCode.M, 'm', ConsoleKey.M, false)]
[InlineData (KeyCode.Z, 'z', ConsoleKey.Z, false)]
public void GetConsoleKeyInfoFromKeyCode_UnshiftedLetterKeys_ReturnsLowercaseChar (
KeyCode key,
char expectedChar,
ConsoleKey expectedKey,
bool expectedShift
)
{
// This test verifies the BUGFIX: Key.A through Key.Z should produce lowercase characters
// when no ShiftMask is set. The old code would incorrectly return uppercase 'A'.
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Assert
Assert.Equal (expectedChar, result.KeyChar);
Assert.Equal (expectedKey, result.Key);
Assert.Equal (expectedShift, (result.Modifiers & ConsoleModifiers.Shift) != 0);
}
[Theory]
[InlineData (KeyCode.A | KeyCode.ShiftMask, 'A', ConsoleKey.A, true)] // Shifted A = uppercase 'A'
[InlineData (KeyCode.B | KeyCode.ShiftMask, 'B', ConsoleKey.B, true)]
[InlineData (KeyCode.M | KeyCode.ShiftMask, 'M', ConsoleKey.M, true)]
[InlineData (KeyCode.Z | KeyCode.ShiftMask, 'Z', ConsoleKey.Z, true)]
public void GetConsoleKeyInfoFromKeyCode_ShiftedLetterKeys_ReturnsUppercaseChar (
KeyCode key,
char expectedChar,
ConsoleKey expectedKey,
bool expectedShift
)
{
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Assert
Assert.Equal (expectedChar, result.KeyChar);
Assert.Equal (expectedKey, result.Key);
Assert.Equal (expectedShift, (result.Modifiers & ConsoleModifiers.Shift) != 0);
}
[Theory]
[InlineData (KeyCode.A | KeyCode.CtrlMask, ConsoleKey.A, false, false, true)]
[InlineData (KeyCode.A | KeyCode.AltMask, ConsoleKey.A, false, true, false)]
[InlineData (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask, ConsoleKey.A, true, false, true)]
[InlineData (KeyCode.Z | KeyCode.CtrlMask, ConsoleKey.Z, false, false, true)]
public void GetConsoleKeyInfoFromKeyCode_LetterKeysWithModifiers_ReturnsCorrectModifiers (
KeyCode key,
ConsoleKey expectedConsoleKey,
bool expectedShift,
bool expectedAlt,
bool expectedControl
)
{
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Assert
Assert.Equal (expectedConsoleKey, result.Key);
Assert.Equal (expectedShift, (result.Modifiers & ConsoleModifiers.Shift) != 0);
Assert.Equal (expectedAlt, (result.Modifiers & ConsoleModifiers.Alt) != 0);
Assert.Equal (expectedControl, (result.Modifiers & ConsoleModifiers.Control) != 0);
}
[Theory]
[InlineData (KeyCode.Enter, '\r', ConsoleKey.Enter)]
[InlineData (KeyCode.Tab, '\t', ConsoleKey.Tab)]
[InlineData (KeyCode.Esc, '\u001B', ConsoleKey.Escape)]
[InlineData (KeyCode.Backspace, '\b', ConsoleKey.Backspace)]
[InlineData (KeyCode.Space, ' ', ConsoleKey.Spacebar)]
public void GetConsoleKeyInfoFromKeyCode_SpecialKeys_ReturnsCorrectKeyChar (
KeyCode key,
char expectedChar,
ConsoleKey expectedKey
)
{
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Assert
Assert.Equal (expectedChar, result.KeyChar);
Assert.Equal (expectedKey, result.Key);
}
[Theory]
[InlineData (KeyCode.F1, ConsoleKey.F1)]
[InlineData (KeyCode.F5, ConsoleKey.F5)]
[InlineData (KeyCode.F12, ConsoleKey.F12)]
[InlineData (KeyCode.CursorUp, ConsoleKey.UpArrow)]
[InlineData (KeyCode.CursorDown, ConsoleKey.DownArrow)]
[InlineData (KeyCode.CursorLeft, ConsoleKey.LeftArrow)]
[InlineData (KeyCode.CursorRight, ConsoleKey.RightArrow)]
[InlineData (KeyCode.Home, ConsoleKey.Home)]
[InlineData (KeyCode.End, ConsoleKey.End)]
[InlineData (KeyCode.PageUp, ConsoleKey.PageUp)]
[InlineData (KeyCode.PageDown, ConsoleKey.PageDown)]
[InlineData (KeyCode.Delete, ConsoleKey.Delete)]
[InlineData (KeyCode.Insert, ConsoleKey.Insert)]
public void GetConsoleKeyInfoFromKeyCode_NavigationAndFunctionKeys_ReturnsCorrectConsoleKey (
KeyCode key,
ConsoleKey expectedKey
)
{
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Assert
Assert.Equal (expectedKey, result.Key);
}
[Theory]
[InlineData (KeyCode.D0, '0', ConsoleKey.D0)]
[InlineData (KeyCode.D1, '1', ConsoleKey.D1)]
[InlineData (KeyCode.D5, '5', ConsoleKey.D5)]
[InlineData (KeyCode.D9, '9', ConsoleKey.D9)]
public void GetConsoleKeyInfoFromKeyCode_NumberKeys_ReturnsCorrectKeyChar (
KeyCode key,
char expectedChar,
ConsoleKey expectedKey
)
{
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Assert
Assert.Equal (expectedChar, result.KeyChar);
Assert.Equal (expectedKey, result.Key);
}
[Fact]
public void MapToConsoleModifiers_AllLetterKeys_UnshiftedDoesNotSetShift ()
{
// This comprehensive test ensures ALL letter keys A-Z without ShiftMask
// do not have Shift set in the returned modifiers.
// This will fail if the old "char.IsUpper" check is present.
for (KeyCode key = KeyCode.A; key <= KeyCode.Z; key++)
{
// Act
ConsoleModifiers result = ConsoleKeyMapping.MapToConsoleModifiers (key);
// Assert
Assert.False (
result.HasFlag (ConsoleModifiers.Shift),
$"Shift should not be set for unshifted {key} (value {(int)key}). " +
$"KeyCode.{key} represents a lowercase, unshifted key even though its numeric value is {(int)key}."
);
}
}
[Fact]
public void MapToConsoleModifiers_AllLetterKeysShifted_SetsShift ()
{
// Verify that WITH ShiftMask, all letter keys DO set Shift
for (KeyCode key = KeyCode.A; key <= KeyCode.Z; key++)
{
KeyCode shiftedKey = key | KeyCode.ShiftMask;
// Act
ConsoleModifiers result = ConsoleKeyMapping.MapToConsoleModifiers (shiftedKey);
// Assert
Assert.True (
result.HasFlag (ConsoleModifiers.Shift),
$"Shift should be set for {shiftedKey}"
);
}
}
[Fact]
public void GetConsoleKeyInfoFromKeyCode_AllUnshiftedLetterKeys_ReturnLowercaseChars ()
{
// This comprehensive test verifies ALL letter keys A-Z produce lowercase characters
// when no ShiftMask is set. This is the KEY test that will fail with the old code.
for (KeyCode key = KeyCode.A; key <= KeyCode.Z; key++)
{
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (key);
// Calculate expected lowercase character
char expectedChar = (char)('a' + (key - KeyCode.A));
// Assert
Assert.True (
char.IsLower (result.KeyChar),
$"KeyChar for unshifted {key} should be lowercase, but got '{result.KeyChar}' (0x{(int)result.KeyChar:X2}). " +
$"Expected lowercase '{expectedChar}' (0x{(int)expectedChar:X2})."
);
Assert.Equal (
expectedChar,
result.KeyChar
);
Assert.False (
(result.Modifiers & ConsoleModifiers.Shift) != 0,
$"Shift modifier should not be set for unshifted {key}"
);
}
}
[Fact]
public void GetConsoleKeyInfoFromKeyCode_AllShiftedLetterKeys_ReturnUppercaseChars ()
{
// Verify that WITH ShiftMask, all letter keys produce uppercase characters
for (KeyCode key = KeyCode.A; key <= KeyCode.Z; key++)
{
KeyCode shiftedKey = key | KeyCode.ShiftMask;
// Act
ConsoleKeyInfo result = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (shiftedKey);
// Calculate expected uppercase character
char expectedChar = (char)('A' + (key - KeyCode.A));
// Assert
Assert.True (
char.IsUpper (result.KeyChar),
$"KeyChar for shifted {shiftedKey} should be uppercase, but got '{result.KeyChar}'"
);
Assert.Equal (expectedChar, result.KeyChar);
Assert.True (
(result.Modifiers & ConsoleModifiers.Shift) != 0,
$"Shift modifier should be set for {shiftedKey}"
);
}
}
[Theory]
[InlineData (KeyCode.A, KeyCode.A | KeyCode.ShiftMask, 'a', 'A')] // Without vs With Shift
[InlineData (KeyCode.M, KeyCode.M | KeyCode.ShiftMask, 'm', 'M')]
[InlineData (KeyCode.Z, KeyCode.Z | KeyCode.ShiftMask, 'z', 'Z')]
public void GetConsoleKeyInfoFromKeyCode_ShiftMaskChangesCase (
KeyCode unshifted,
KeyCode shifted,
char expectedUnshiftedChar,
char expectedShiftedChar
)
{
// Act
ConsoleKeyInfo unshiftedResult = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (unshifted);
ConsoleKeyInfo shiftedResult = ConsoleKeyMapping.GetConsoleKeyInfoFromKeyCode (shifted);
// Assert - Unshifted should be lowercase
Assert.Equal (expectedUnshiftedChar, unshiftedResult.KeyChar);
Assert.False ((unshiftedResult.Modifiers & ConsoleModifiers.Shift) != 0);
// Assert - Shifted should be uppercase
Assert.Equal (expectedShiftedChar, shiftedResult.KeyChar);
Assert.True ((shiftedResult.Modifiers & ConsoleModifiers.Shift) != 0);
}
}

View File

@@ -0,0 +1,137 @@
using System.Text;
using UnitTests;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests_Parallelizable.DriverTests;
public class ContentsTests (ITestOutputHelper output) : FakeDriverBase
{
[Fact]
public void AddStr_Combining_Character_1st_Column ()
{
IDriver driver = CreateFakeDriver ();
var expected = "\u0301!";
driver.AddStr ("\u0301!"); // acute accent + exclamation mark
DriverAssert.AssertDriverContentsAre (expected, output, driver);
driver.End ();
}
[Fact]
public void AddStr_With_Combining_Characters ()
{
IDriver driver = CreateFakeDriver ();
var acuteAccent = new Rune (0x0301); // Combining acute accent (é)
string combined = "e" + acuteAccent;
var expected = "é";
driver.AddStr (combined);
DriverAssert.AssertDriverContentsAre (expected, output, driver);
// 3 char combine
// a + ogonek + acute = <U+0061, U+0328, U+0301> ( ą́ )
var oGonek = new Rune (0x0328); // Combining ogonek (a small hook or comma shape)
combined = "a" + oGonek + acuteAccent;
expected = ("a" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616
driver.Move (0, 0);
driver.AddStr (combined);
DriverAssert.AssertDriverContentsAre (expected, output, driver);
// e + ogonek + acute = <U+0061, U+0328, U+0301> ( ę́́ )
combined = "e" + oGonek + acuteAccent;
expected = ("e" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616
driver.Move (0, 0);
driver.AddStr (combined);
DriverAssert.AssertDriverContentsAre (expected, output, driver);
// i + ogonek + acute = <U+0061, U+0328, U+0301> ( į́́́ )
combined = "i" + oGonek + acuteAccent;
expected = ("i" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616
driver.Move (0, 0);
driver.AddStr (combined);
DriverAssert.AssertDriverContentsAre (expected, output, driver);
// u + ogonek + acute = <U+0061, U+0328, U+0301> ( ų́́́́ )
combined = "u" + oGonek + acuteAccent;
expected = ("u" + oGonek).Normalize (NormalizationForm.FormC); // See Issue #2616
driver.Move (0, 0);
driver.AddStr (combined);
DriverAssert.AssertDriverContentsAre (expected, output, driver);
driver.End ();
}
[Fact]
public void Move_Bad_Coordinates ()
{
IDriver driver = CreateFakeDriver ();
Assert.Equal (0, driver.Col);
Assert.Equal (0, driver.Row);
driver.Move (-1, 0);
Assert.Equal (-1, driver.Col);
Assert.Equal (0, driver.Row);
driver.Move (0, -1);
Assert.Equal (0, driver.Col);
Assert.Equal (-1, driver.Row);
driver.Move (driver.Cols, 0);
Assert.Equal (driver.Cols, driver.Col);
Assert.Equal (0, driver.Row);
driver.Move (0, driver.Rows);
Assert.Equal (0, driver.Col);
Assert.Equal (driver.Rows, driver.Row);
driver.Move (500, 500);
Assert.Equal (500, driver.Col);
Assert.Equal (500, driver.Row);
driver.End ();
}
// TODO: Add these unit tests
// AddRune moves correctly
// AddRune with wide characters are handled correctly
// AddRune with wide characters and Col < 0 are handled correctly
// AddRune with wide characters and Col == Cols - 1 are handled correctly
// AddRune with wide characters and Col == Cols are handled correctly
// AddStr moves correctly
// AddStr with wide characters moves correctly
// AddStr where Col is negative works
// AddStr where Col is negative and characters include wide / combining characters works
// AddStr where Col is near Cols and characters include wide / combining characters works
// Clipping works correctly
// Clipping works correctly with wide characters
// Clipping works correctly with combining characters
// Clipping works correctly with combining characters and wide characters
// ResizeScreen works correctly
// Refresh works correctly
// IsDirty tests
}

View File

@@ -0,0 +1,19 @@
// Alias Console to MockConsole so we don't accidentally use Console
using UnitTests;
namespace UnitTests_Parallelizable.DriverTests;
public class DriverColorTests : FakeDriverBase
{
[Fact]
public void Force16Colors_Sets ()
{
IDriver driver = CreateFakeDriver ();
driver.Force16Colors = true;
Assert.True (driver.Force16Colors);
driver.End ();
}
}

View File

@@ -0,0 +1,6 @@
using UnitTests;
namespace UnitTests_Parallelizable.DriverTests;
public class DriverTests : FakeDriverBase
{ }

View File

@@ -1,8 +1,8 @@
using UnitTests.Parallelizable;
using UnitTests;
namespace UnitTests_Parallelizable.DriverTests;
public class EscSeqRequestsTests : ParallelizableBase
public class EscSeqRequestsTests : FakeDriverBase
{
[Fact]
public void Add_Tests ()

View File

@@ -0,0 +1,106 @@
using System.Text;
// ReSharper disable HeuristicUnreachableCode
namespace UnitTests_Parallelizable.DriverTests;
public class EscSeqUtilsTests
{
[Fact]
public void Defaults_Values ()
{
Assert.Equal ('\x1b', EscSeqUtils.KeyEsc);
Assert.Equal ("\x1b[", EscSeqUtils.CSI);
Assert.Equal ("\x1b[?1003h", EscSeqUtils.CSI_EnableAnyEventMouse);
Assert.Equal ("\x1b[?1006h", EscSeqUtils.CSI_EnableSgrExtModeMouse);
Assert.Equal ("\x1b[?1015h", EscSeqUtils.CSI_EnableUrxvtExtModeMouse);
Assert.Equal ("\x1b[?1003l", EscSeqUtils.CSI_DisableAnyEventMouse);
Assert.Equal ("\x1b[?1006l", EscSeqUtils.CSI_DisableSgrExtModeMouse);
Assert.Equal ("\x1b[?1015l", EscSeqUtils.CSI_DisableUrxvtExtModeMouse);
Assert.Equal ("\x1b[?1003h\x1b[?1015h\u001b[?1006h", EscSeqUtils.CSI_EnableMouseEvents);
Assert.Equal ("\x1b[?1003l\x1b[?1015l\u001b[?1006l", EscSeqUtils.CSI_DisableMouseEvents);
}
[Fact]
public void GetConsoleInputKey_ConsoleKeyInfo ()
{
var cki = new ConsoleKeyInfo ('r', 0, false, false, false);
var expectedCki = new ConsoleKeyInfo ('r', ConsoleKey.R, false, false, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo (cki).ToString ());
cki = new ('r', 0, true, false, false);
expectedCki = new ('r', ConsoleKey.R, true, false, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo (cki).ToString ());
cki = new ('r', 0, false, true, false);
expectedCki = new ('r', ConsoleKey.R, false, true, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo (cki).ToString ());
cki = new ('r', 0, false, false, true);
expectedCki = new ('r', ConsoleKey.R, false, false, true);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('r', 0, true, true, false);
expectedCki = new ('r', ConsoleKey.R, true, true, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo (cki).ToString ());
cki = new ('r', 0, false, true, true);
expectedCki = new ('r', ConsoleKey.R, false, true, true);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('r', 0, true, true, true);
expectedCki = new ('r', ConsoleKey.R, true, true, true);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('\u0012', 0, false, false, false);
expectedCki = new ('\u0012', ConsoleKey.R, false, false, true);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('\0', (ConsoleKey)64, false, false, true);
expectedCki = new ('\0', ConsoleKey.Spacebar, false, false, true);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('\r', 0, false, false, false);
expectedCki = new ('\r', ConsoleKey.Enter, false, false, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('\u007f', 0, false, false, false);
expectedCki = new ('\u007f', ConsoleKey.Backspace, false, false, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
cki = new ('R', 0, false, false, false);
expectedCki = new ('R', ConsoleKey.R, true, false, false);
Assert.Equal (expectedCki.ToString(), EscSeqUtils.MapConsoleKeyInfo(cki).ToString());
}
[Theory]
[InlineData (0, 0, $"{EscSeqUtils.CSI}0;0H")]
[InlineData (int.MaxValue, int.MaxValue, $"{EscSeqUtils.CSI}2147483647;2147483647H")]
[InlineData (int.MinValue, int.MinValue, $"{EscSeqUtils.CSI}-2147483648;-2147483648H")]
public void CSI_WriteCursorPosition_ReturnsCorrectEscSeq (int row, int col, string expected)
{
StringBuilder builder = new();
using StringWriter writer = new(builder);
EscSeqUtils.CSI_WriteCursorPosition (writer, row, col);
string actual = builder.ToString();
Assert.Equal (expected, actual);
}
[Theory]
[InlineData ('\u001B', KeyCode.Esc)]
[InlineData ('\r', KeyCode.Enter)]
[InlineData ('1', KeyCode.D1)]
[InlineData ('!', (KeyCode)'!')]
[InlineData ('a', KeyCode.A)]
[InlineData ('A', KeyCode.A | KeyCode.ShiftMask)]
public void MapChar_Returns_Modifiers_If_Needed (char ch, KeyCode keyCode)
{
ConsoleKeyInfo cki = EscSeqUtils.MapChar (ch);
Key key = EscSeqUtils.MapKey (cki);
Key expectedKey = keyCode;
Assert.Equal (key, expectedKey);
}
}

View File

@@ -0,0 +1,286 @@
using System.Text;
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
/// <summary>
/// Tests for the FakeDriver to ensure it works properly with the modern component factory architecture.
/// </summary>
public class FakeDriverTests (ITestOutputHelper output) : FakeDriverBase
{
private readonly ITestOutputHelper _output = output;
#region Basic FakeDriver Tests
[Fact]
public void FakeDriver_Init_Works ()
{
IDriver driver = CreateFakeDriver ();
Assert.IsAssignableFrom<IDriver> (driver);
_output.WriteLine ($"Driver type: {driver.GetType ().Name}");
_output.WriteLine ($"Screen size: {driver.Screen}");
}
[Fact]
public void FakeDriver_Screen_Has_Default_Size ()
{
IDriver driver = CreateFakeDriver ();
// Default size should be 80x25
Assert.Equal (new (0, 0, 80, 25), driver.Screen);
Assert.Equal (80, driver.Cols);
Assert.Equal (25, driver.Rows);
}
[Fact]
public void FakeDriver_Can_Resize ()
{
IDriver driver = CreateFakeDriver ();
// Start with default size
Assert.Equal (80, driver.Cols);
Assert.Equal (25, driver.Rows);
// Resize to 100x30
driver?.SetScreenSize (100, 30);
// Verify new size
Assert.Equal (100, driver.Cols);
Assert.Equal (30, driver.Rows);
Assert.Equal (new (0, 0, 100, 30), driver.Screen);
}
#endregion
#region CreateFakeDriver Tests
[Fact]
public void SetupFakeDriver_Initializes_Driver_With_80x25 ()
{
IDriver driver = CreateFakeDriver ();
Assert.NotNull (driver);
Assert.Equal (new (0, 0, 80, 25), driver.Screen);
Assert.Equal (80, driver.Cols);
Assert.Equal (25, driver.Rows);
}
[Fact]
public void SetupFakeDriver_Driver_Is_IDriver ()
{
IDriver driver = CreateFakeDriver ();
Assert.NotNull (driver);
// Should be IDriver
Assert.IsAssignableFrom<IDriver> (driver);
_output.WriteLine ($"Driver type: {driver.GetType ().Name}");
}
[Fact]
public void SetupFakeDriver_Can_Set_Screen_Size ()
{
IDriver driver = CreateFakeDriver ();
IDriver fakeDriver = driver;
Assert.NotNull (fakeDriver);
fakeDriver!.SetScreenSize (100, 50);
Assert.Equal (100, driver.Cols);
Assert.Equal (50, driver.Rows);
}
#endregion
#region Clipboard Tests
[Fact]
public void FakeDriver_Clipboard_Works_When_Enabled ()
{
IDriver driver = CreateFakeDriver ();
Assert.NotNull (driver.Clipboard);
Assert.True (driver.Clipboard.IsSupported);
// Set clipboard content
driver.Clipboard.SetClipboardData ("Test content");
// Get clipboard content
string content = driver.Clipboard.GetClipboardData ();
Assert.Equal ("Test content", content);
}
[Fact]
public void FakeDriver_Clipboard_GetClipboarData_Works ()
{
IDriver driver = CreateFakeDriver ();
Assert.NotNull (driver.Clipboard);
driver.Clipboard.SetClipboardData ("test");
Assert.Equal ("test", driver.Clipboard.GetClipboardData ());
}
#endregion
#region Buffer and Fill Tests
[Fact]
public void FakeDriver_Can_Fill_Rectangle ()
{
IDriver driver = CreateFakeDriver ();
// Verify driver is initialized with buffers
Assert.NotNull (driver);
Assert.NotNull (driver.Contents);
// Fill a rectangle
var rect = new Rectangle (5, 5, 10, 5);
driver.FillRect (rect, (Rune)'X');
// Verify the rectangle was filled
for (int row = rect.Y; row < rect.Y + rect.Height; row++)
{
for (int col = rect.X; col < rect.X + rect.Width; col++)
{
Assert.Equal ((Rune)'X', driver.Contents [row, col].Rune);
}
}
}
[Fact]
public void FakeDriver_Buffer_Integrity_After_Multiple_Resizes ()
{
IDriver driver = CreateFakeDriver ();
// Start with default size
Assert.Equal (80, driver.Cols);
Assert.Equal (25, driver.Rows);
// Fill with a pattern
driver.FillRect (new (0, 0, 10, 5), (Rune)'A');
// Resize
driver?.SetScreenSize (100, 30);
// Verify new size
Assert.Equal (100, driver.Cols);
Assert.Equal (30, driver.Rows);
// Verify buffer is clean (no stale runes from previous size)
Assert.NotNull (driver.Contents);
Assert.Equal (30, driver.Contents!.GetLength (0));
Assert.Equal (100, driver.Contents.GetLength (1));
// Fill with new pattern
driver.FillRect (new (0, 0, 20, 10), (Rune)'B');
// Resize back
driver?.SetScreenSize (80, 25);
// Verify size is back
Assert.Equal (80, driver.Cols);
Assert.Equal (25, driver.Rows);
// Verify buffer dimensions match
Assert.Equal (25, driver.Contents.GetLength (0));
Assert.Equal (80, driver.Contents.GetLength (1));
}
#endregion
#region ScreenChanged Event Tests
[Fact]
public void ScreenChanged_Event_Fires_On_SetScreenSize ()
{
IDriver driver = CreateFakeDriver ();
var screenChangedFired = false;
Size? newSize = null;
driver.SizeChanged += (sender, args) =>
{
screenChangedFired = true;
newSize = args.Size;
};
// Trigger resize using FakeResize which uses SetScreenSize internally
driver?.SetScreenSize (100, 30);
// Verify event fired
Assert.True (screenChangedFired);
Assert.NotNull (newSize);
Assert.Equal (100, newSize!.Value.Width);
Assert.Equal (30, newSize.Value.Height);
}
[Fact]
public void FakeResize_Triggers_ScreenChanged_And_Updates_Application_Screen ()
{
IDriver driver = CreateFakeDriver ();
var screenChangedFired = false;
Size? eventSize = null;
driver.SizeChanged += (sender, args) =>
{
screenChangedFired = true;
eventSize = args.Size;
};
// Use FakeResize helper
driver?.SetScreenSize (120, 40);
// Verify event fired
Assert.True (screenChangedFired);
Assert.NotNull (eventSize);
Assert.Equal (120, eventSize!.Value.Width);
Assert.Equal (40, eventSize.Value.Height);
// Verify driver.Screen was updated
Assert.Equal (new (0, 0, 120, 40), driver.Screen);
Assert.Equal (120, driver.Cols);
Assert.Equal (40, driver.Rows);
}
[Fact]
public void SizeChanged_Event_Still_Fires_For_Compatibility ()
{
IDriver driver = CreateFakeDriver ();
var sizeChangedFired = false;
var screenChangedFired = false;
#pragma warning disable CS0618 // Type or member is obsolete
driver.SizeChanged += (sender, args) => { sizeChangedFired = true; };
#pragma warning restore CS0618 // Type or member is obsolete
driver.SizeChanged += (sender, args) => { screenChangedFired = true; };
// Trigger resize using FakeResize
driver?.SetScreenSize (90, 35);
// Both events should fire for compatibility
Assert.True (sizeChangedFired);
Assert.True (screenChangedFired);
}
#endregion
}

View File

@@ -0,0 +1,212 @@
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
public class KeyCodeTests
{
private readonly ITestOutputHelper _output;
public KeyCodeTests (ITestOutputHelper output) { _output = output; }
[Fact]
public void Key_Enum_Ambiguity_Check ()
{
KeyCode key = KeyCode.Y | KeyCode.CtrlMask;
// This will not be well compared.
Assert.True (key.HasFlag (KeyCode.Q | KeyCode.CtrlMask));
Assert.True ((key & (KeyCode.Q | KeyCode.CtrlMask)) != 0);
Assert.Equal (KeyCode.Y | KeyCode.CtrlMask, key);
Assert.Equal ("Y, CtrlMask", key.ToString ());
// This will be well compared, because the Key.CtrlMask have a high value.
Assert.False (key == Application.QuitKey);
switch (key)
{
case KeyCode.Q | KeyCode.CtrlMask:
// Never goes here.
break;
case KeyCode.Y | KeyCode.CtrlMask:
Assert.True (key == (KeyCode.Y | KeyCode.CtrlMask));
break;
}
}
[Fact]
public void Key_ToString ()
{
KeyCode k = KeyCode.Y | KeyCode.CtrlMask;
Assert.Equal ("Y, CtrlMask", k.ToString ());
k = KeyCode.CtrlMask | KeyCode.Y;
Assert.Equal ("Y, CtrlMask", k.ToString ());
k = KeyCode.Space;
Assert.Equal ("Space", k.ToString ());
k = KeyCode.D;
Assert.Equal ("D", k.ToString ());
k = (KeyCode)'d';
Assert.Equal ("d", ((char)k).ToString ());
k = KeyCode.D;
Assert.Equal ("D", k.ToString ());
// In a console this will always returns Key.D
k = KeyCode.D | KeyCode.ShiftMask;
Assert.Equal ("D, ShiftMask", k.ToString ());
}
[Fact]
public void KeyEnum_ShouldHaveCorrectValues ()
{
Assert.Equal (0, (int)KeyCode.Null);
Assert.Equal (8, (int)KeyCode.Backspace);
Assert.Equal (9, (int)KeyCode.Tab);
// Continue for other keys...
}
[Fact]
public void SimpleEnum_And_FlagedEnum ()
{
SimpleEnum simple = SimpleEnum.Three | SimpleEnum.Five;
// Nothing will not be well compared here.
Assert.True (simple.HasFlag (SimpleEnum.Zero | SimpleEnum.Five));
Assert.True (simple.HasFlag (SimpleEnum.One | SimpleEnum.Five));
Assert.True (simple.HasFlag (SimpleEnum.Two | SimpleEnum.Five));
Assert.True (simple.HasFlag (SimpleEnum.Three | SimpleEnum.Five));
Assert.True (simple.HasFlag (SimpleEnum.Four | SimpleEnum.Five));
Assert.True ((simple & (SimpleEnum.Zero | SimpleEnum.Five)) != 0);
Assert.True ((simple & (SimpleEnum.One | SimpleEnum.Five)) != 0);
Assert.True ((simple & (SimpleEnum.Two | SimpleEnum.Five)) != 0);
Assert.True ((simple & (SimpleEnum.Three | SimpleEnum.Five)) != 0);
Assert.True ((simple & (SimpleEnum.Four | SimpleEnum.Five)) != 0);
Assert.Equal (7, (int)simple); // As it is not flagged only shows as number.
Assert.Equal ("7", simple.ToString ());
Assert.False (simple == (SimpleEnum.Zero | SimpleEnum.Five));
Assert.False (simple == (SimpleEnum.One | SimpleEnum.Five));
Assert.True (simple == (SimpleEnum.Two | SimpleEnum.Five));
Assert.True (simple == (SimpleEnum.Three | SimpleEnum.Five));
Assert.False (simple == (SimpleEnum.Four | SimpleEnum.Five));
FlaggedEnum flagged = FlaggedEnum.Three | FlaggedEnum.Five;
// Nothing will not be well compared here.
Assert.True (flagged.HasFlag (FlaggedEnum.Zero | FlaggedEnum.Five));
Assert.True (flagged.HasFlag (FlaggedEnum.One | FlaggedEnum.Five));
Assert.True (flagged.HasFlag (FlaggedEnum.Two | FlaggedEnum.Five));
Assert.True (flagged.HasFlag (FlaggedEnum.Three | FlaggedEnum.Five));
Assert.True (flagged.HasFlag (FlaggedEnum.Four | FlaggedEnum.Five));
Assert.True ((flagged & (FlaggedEnum.Zero | FlaggedEnum.Five)) != 0);
Assert.True ((flagged & (FlaggedEnum.One | FlaggedEnum.Five)) != 0);
Assert.True ((flagged & (FlaggedEnum.Two | FlaggedEnum.Five)) != 0);
Assert.True ((flagged & (FlaggedEnum.Three | FlaggedEnum.Five)) != 0);
Assert.True ((flagged & (FlaggedEnum.Four | FlaggedEnum.Five)) != 0);
Assert.Equal (FlaggedEnum.Two | FlaggedEnum.Five, flagged); // As it is flagged shows as bitwise.
Assert.Equal ("Two, Five", flagged.ToString ());
Assert.False (flagged == (FlaggedEnum.Zero | FlaggedEnum.Five));
Assert.False (flagged == (FlaggedEnum.One | FlaggedEnum.Five));
Assert.True (flagged == (FlaggedEnum.Two | FlaggedEnum.Five));
Assert.True (flagged == (FlaggedEnum.Three | FlaggedEnum.Five));
Assert.False (flagged == (FlaggedEnum.Four | FlaggedEnum.Five));
}
[Fact]
public void SimpleHighValueEnum_And_FlaggedHighValueEnum ()
{
SimpleHighValueEnum simple = SimpleHighValueEnum.Three | SimpleHighValueEnum.Last;
// This will not be well compared.
Assert.True (simple.HasFlag (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last));
Assert.True (simple.HasFlag (SimpleHighValueEnum.One | SimpleHighValueEnum.Last));
Assert.True (simple.HasFlag (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last));
Assert.True (simple.HasFlag (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last));
Assert.False (simple.HasFlag (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last));
Assert.True ((simple & (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)) != 0);
Assert.True ((simple & (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)) != 0);
Assert.True ((simple & (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)) != 0);
Assert.True ((simple & (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)) != 0);
Assert.True ((simple & (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)) != 0);
// This will be well compared, because the SimpleHighValueEnum.Last have a high value.
Assert.Equal (1073741827, (int)simple); // As it is not flagged only shows as number.
Assert.Equal ("1073741827", simple.ToString ()); // As it is not flagged only shows as number.
Assert.False (simple == (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last));
Assert.False (simple == (SimpleHighValueEnum.One | SimpleHighValueEnum.Last));
Assert.False (simple == (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last));
Assert.True (simple == (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last));
Assert.False (simple == (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last));
FlaggedHighValueEnum flagged = FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last;
// This will not be well compared.
Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last));
Assert.True (flagged.HasFlag (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last));
Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last));
Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last));
Assert.False (flagged.HasFlag (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last));
Assert.True ((flagged & (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)) != 0);
Assert.True ((flagged & (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)) != 0);
Assert.True ((flagged & (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)) != 0);
Assert.True ((flagged & (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)) != 0);
Assert.True ((flagged & (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)) != 0);
// This will be well compared, because the SimpleHighValueEnum.Last have a high value.
Assert.Equal (
FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last,
flagged
); // As it is flagged shows as bitwise.
Assert.Equal ("Three, Last", flagged.ToString ()); // As it is flagged shows as bitwise.
Assert.False (flagged == (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last));
Assert.False (flagged == (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last));
Assert.False (flagged == (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last));
Assert.True (flagged == (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last));
Assert.False (flagged == (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last));
}
[Flags]
private enum FlaggedEnum
{
Zero,
One,
Two,
Three,
Four,
Five
}
[Flags]
private enum FlaggedHighValueEnum
{
Zero,
One,
Two,
Three,
Four,
Last = 0x40000000
}
private enum SimpleEnum
{
Zero,
One,
Two,
Three,
Four,
Five
}
private enum SimpleHighValueEnum
{
Zero,
One,
Two,
Three,
Four,
Last = 0x40000000
}
}

View File

@@ -0,0 +1,535 @@
#nullable enable
using System.Collections.Concurrent;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace UnitTests_Parallelizable.DriverTests;
/// <summary>
/// Low-level tests for IInput and IOutput implementations across all drivers.
/// These tests are designed to fail with good error messages when run in environments
/// without a real terminal (like GitHub Actions).
/// </summary>
public class IInputOutputTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
#region DotNet Driver Tests
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetInput_Constructor_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
using var input = new NetInput ();
_output.WriteLine ("NetInput created successfully");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetInput constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetInput_Peek_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange
using var input = new NetInput ();
// Act
Exception? exception = Record.Exception (() =>
{
bool hasInput = input.Peek ();
_output.WriteLine ($"NetInput.Peek() returned: {hasInput}");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetInput.Peek() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetInput_Read_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange
using var input = new NetInput ();
// Act
Exception? exception = Record.Exception (() =>
{
List<ConsoleKeyInfo> items = input.Read ().ToList ();
_output.WriteLine ($"NetInput.Read() returned {items.Count} items");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetInput.Read() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetInput_Dispose_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange
var input = new NetInput ();
// Act
Exception? exception = Record.Exception (() =>
{
input.Dispose ();
_output.WriteLine ("NetInput disposed successfully");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetInput.Dispose() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetOutput_Constructor_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
using var output = new NetOutput ();
_output.WriteLine ("NetOutput created successfully");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetOutput constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetOutput_GetSize_ReturnsDefaultSize_WhenNoTerminalAvailable ()
{
// Arrange
using var output = new NetOutput ();
// Act
Size size = default;
Exception? exception = Record.Exception (() =>
{
size = output.GetSize ();
_output.WriteLine ($"NetOutput.GetSize() returned: {size.Width}x{size.Height}");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetOutput.GetSize() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
Assert.True (size.Width > 0, "Width should be > 0");
Assert.True (size.Height > 0, "Height should be > 0");
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetOutput_Write_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange
using var output = new NetOutput ();
// Act
Exception? exception = Record.Exception (() =>
{
ReadOnlySpan<char> text = "Test".AsSpan ();
output.Write (text);
_output.WriteLine ("NetOutput.Write() succeeded");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetOutput.Write() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetOutput_SetCursorPosition_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange
using var output = new NetOutput ();
// Act
Exception? exception = Record.Exception (() =>
{
output.SetCursorPosition (0, 0);
_output.WriteLine ("NetOutput.SetCursorPosition() succeeded");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetOutput.SetCursorPosition() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetInputProcessor_Constructor_DoesNotThrow ()
{
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
var processor = new NetInputProcessor (queue);
_output.WriteLine ("NetInputProcessor created successfully");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetInputProcessor constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
#endregion
#region Unix Driver Tests
[Fact]
[Trait ("Category", "LowLevelDriver")]
[Trait ("Platform", "Unix")]
public void UnixInput_Constructor_DoesNotThrow_WhenNoTerminalAvailable ()
{
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
try
{
using var input = new UnixInput ();
_output.WriteLine ("UnixInput created successfully");
}
catch (Exception ex)
{
_output.WriteLine ($"Expected failure on non-terminal: {ex.Message}");
throw new XunitException (
$"UnixInput failed in non-terminal environment: {ex.Message}\nThis is expected in GitHub Actions. The driver should detect this and handle gracefully.");
}
});
// Assert
if (exception != null && !(exception is XunitException))
{
_output.WriteLine ($"FAILED: UnixInput constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
[Trait ("Platform", "Unix")]
public void UnixOutput_Constructor_DoesNotThrow_WhenNoTerminalAvailable ()
{
if (OperatingSystem.IsWindows ())
{
_output.WriteLine ("Skipping Unix test on Windows");
return;
}
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
using var output = new UnixOutput ();
_output.WriteLine ("UnixOutput created successfully");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: UnixOutput constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
[Trait ("Platform", "Unix")]
public void UnixOutput_GetSize_ReturnsDefaultSize_WhenNoTerminalAvailable ()
{
if (OperatingSystem.IsWindows ())
{
_output.WriteLine ("Skipping Unix test on Windows");
return;
}
// Arrange
using var output = new UnixOutput ();
// Act
Size size = default;
Exception? exception = Record.Exception (() =>
{
size = output.GetSize ();
_output.WriteLine ($"UnixOutput.GetSize() returned: {size.Width}x{size.Height}");
});
// Assert
Assert.Null (exception);
Assert.Equal (80, size.Width);
Assert.Equal (25, size.Height);
}
#endregion
#region Windows Driver Tests
[Fact]
[Trait ("Category", "LowLevelDriver")]
[Trait ("Platform", "Windows")]
public void WindowsInput_Constructor_DoesNotThrow_WhenNoTerminalAvailable ()
{
if (!OperatingSystem.IsWindows ())
{
_output.WriteLine ("Skipping Windows test on non-Windows");
return;
}
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
using var input = new WindowsInput ();
_output.WriteLine ("WindowsInput created successfully");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: WindowsInput constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
[Trait ("Platform", "Windows")]
public void WindowsOutput_Constructor_DoesNotThrow_WhenNoTerminalAvailable ()
{
if (!OperatingSystem.IsWindows ())
{
_output.WriteLine ("Skipping Windows test on non-Windows");
return;
}
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
try
{
using var output = new WindowsOutput ();
_output.WriteLine ("WindowsOutput created successfully");
}
catch (Exception ex)
{
_output.WriteLine ($"WindowsOutput threw during construction: {ex.GetType ().Name}: {ex.Message}");
throw;
}
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: WindowsOutput constructor threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
#endregion
#region Fake Driver Tests
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void FakeInput_Constructor_DoesNotThrow ()
{
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
using var input = new FakeInput ();
_output.WriteLine ("FakeInput created successfully");
});
// Assert
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void FakeOutput_Constructor_DoesNotThrow ()
{
// Arrange & Act
Exception? exception = Record.Exception (() =>
{
using var output = new FakeOutput ();
_output.WriteLine ("FakeOutput created successfully");
});
// Assert
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void FakeOutput_GetSize_ReturnsExpectedSize ()
{
// Arrange
using var output = new FakeOutput ();
// Act
Size size = output.GetSize ();
_output.WriteLine ($"FakeOutput.GetSize() returned: {size.Width}x{size.Height}");
// Assert
Assert.True (size.Width > 0);
Assert.True (size.Height > 0);
}
#endregion
#region Component Factory Tests
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetComponentFactory_CreateInput_DoesNotThrow ()
{
// Arrange
var factory = new NetComponentFactory ();
// Act
Exception? exception = Record.Exception (() =>
{
using IInput<ConsoleKeyInfo> input = factory.CreateInput ();
_output.WriteLine ("NetComponentFactory.CreateInput() succeeded");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetComponentFactory.CreateInput() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void NetComponentFactory_CreateOutput_DoesNotThrow ()
{
// Arrange
var factory = new NetComponentFactory ();
// Act
Exception? exception = Record.Exception (() =>
{
using IOutput output = factory.CreateOutput ();
_output.WriteLine ("NetComponentFactory.CreateOutput() succeeded");
});
// Assert
if (exception != null)
{
_output.WriteLine ($"FAILED: NetComponentFactory.CreateOutput() threw: {exception.GetType ().Name}: {exception.Message}");
_output.WriteLine ($"Stack trace: {exception.StackTrace}");
}
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void FakeComponentFactory_CreateInput_DoesNotThrow ()
{
// Arrange
var factory = new FakeComponentFactory ();
// Act
Exception? exception = Record.Exception (() =>
{
using IInput<ConsoleKeyInfo> input = factory.CreateInput ();
_output.WriteLine ("FakeComponentFactory.CreateInput() succeeded");
});
// Assert
Assert.Null (exception);
}
[Fact]
[Trait ("Category", "LowLevelDriver")]
public void FakeComponentFactory_CreateOutput_DoesNotThrow ()
{
// Arrange
var factory = new FakeComponentFactory ();
// Act
Exception? exception = Record.Exception (() =>
{
using IOutput output = factory.CreateOutput ();
_output.WriteLine ("FakeComponentFactory.CreateOutput() succeeded");
});
// Assert
Assert.Null (exception);
}
#endregion
}

View File

@@ -0,0 +1,153 @@
namespace UnitTests_Parallelizable.DriverTests;
public class MouseInterpreterTests
{
[Theory]
[MemberData (nameof (SequenceTests))]
public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> events, params MouseFlags? [] expected)
{
// Arrange: Mock dependencies and set up the interpreter
var interpreter = new MouseInterpreter ();
// Act and Assert
for (var i = 0; i < events.Count; i++)
{
MouseEventArgs [] results = interpreter.Process (events [i]).ToArray ();
// Raw input event should be there
Assert.Equal (events [i].Flags, results [0].Flags);
// also any expected should be there
if (expected [i] != null)
{
Assert.Equal (expected [i], results [1].Flags);
}
else
{
Assert.Single (results);
}
}
}
public static IEnumerable<object []> SequenceTests ()
{
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button1Pressed },
new ()
},
null,
MouseFlags.Button1Clicked
];
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button1Pressed },
new (),
new () { Flags = MouseFlags.Button1Pressed },
new ()
},
null,
MouseFlags.Button1Clicked,
null,
MouseFlags.Button1DoubleClicked
];
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button1Pressed },
new (),
new () { Flags = MouseFlags.Button1Pressed },
new (),
new () { Flags = MouseFlags.Button1Pressed },
new ()
},
null,
MouseFlags.Button1Clicked,
null,
MouseFlags.Button1DoubleClicked,
null,
MouseFlags.Button1TripleClicked
];
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button2Pressed },
new (),
new () { Flags = MouseFlags.Button2Pressed },
new (),
new () { Flags = MouseFlags.Button2Pressed },
new ()
},
null,
MouseFlags.Button2Clicked,
null,
MouseFlags.Button2DoubleClicked,
null,
MouseFlags.Button2TripleClicked
];
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button3Pressed },
new (),
new () { Flags = MouseFlags.Button3Pressed },
new (),
new () { Flags = MouseFlags.Button3Pressed },
new ()
},
null,
MouseFlags.Button3Clicked,
null,
MouseFlags.Button3DoubleClicked,
null,
MouseFlags.Button3TripleClicked
];
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button4Pressed },
new (),
new () { Flags = MouseFlags.Button4Pressed },
new (),
new () { Flags = MouseFlags.Button4Pressed },
new ()
},
null,
MouseFlags.Button4Clicked,
null,
MouseFlags.Button4DoubleClicked,
null,
MouseFlags.Button4TripleClicked
];
yield return
[
new List<MouseEventArgs>
{
new () { Flags = MouseFlags.Button1Pressed, Position = new (10, 11) },
new () { Position = new (10, 11) },
// Clicking the line below means no double click because it's a different location
new () { Flags = MouseFlags.Button1Pressed, Position = new (10, 12) },
new () { Position = new (10, 12) }
},
null,
MouseFlags.Button1Clicked,
null,
MouseFlags.Button1Clicked //release is click because new position
];
}
}

View File

@@ -0,0 +1,119 @@
using System.Collections.Concurrent;
using System.Text;
namespace UnitTests_Parallelizable.DriverTests;
public class NetInputProcessorTests
{
public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Rune ()
{
yield return [new ConsoleKeyInfo ('C', ConsoleKey.None, false, false, false), new Rune ('C')];
yield return [new ConsoleKeyInfo ('\\', ConsoleKey.Oem5, false, false, false), new Rune ('\\')];
yield return [new ConsoleKeyInfo ('+', ConsoleKey.OemPlus, true, false, false), new Rune ('+')];
yield return [new ConsoleKeyInfo ('=', ConsoleKey.OemPlus, false, false, false), new Rune ('=')];
yield return new object [] { new ConsoleKeyInfo ('_', ConsoleKey.OemMinus, true, false, false), new Rune ('_') };
yield return new object [] { new ConsoleKeyInfo ('-', ConsoleKey.OemMinus, false, false, false), new Rune ('-') };
yield return new object [] { new ConsoleKeyInfo (')', ConsoleKey.None, false, false, false), new Rune (')') };
yield return new object [] { new ConsoleKeyInfo ('0', ConsoleKey.None, false, false, false), new Rune ('0') };
yield return new object [] { new ConsoleKeyInfo ('(', ConsoleKey.None, false, false, false), new Rune ('(') };
yield return new object [] { new ConsoleKeyInfo ('9', ConsoleKey.None, false, false, false), new Rune ('9') };
yield return new object [] { new ConsoleKeyInfo ('*', ConsoleKey.None, false, false, false), new Rune ('*') };
yield return new object [] { new ConsoleKeyInfo ('8', ConsoleKey.None, false, false, false), new Rune ('8') };
yield return new object [] { new ConsoleKeyInfo ('&', ConsoleKey.None, false, false, false), new Rune ('&') };
yield return new object [] { new ConsoleKeyInfo ('7', ConsoleKey.None, false, false, false), new Rune ('7') };
yield return new object [] { new ConsoleKeyInfo ('^', ConsoleKey.None, false, false, false), new Rune ('^') };
yield return new object [] { new ConsoleKeyInfo ('6', ConsoleKey.None, false, false, false), new Rune ('6') };
yield return new object [] { new ConsoleKeyInfo ('%', ConsoleKey.None, false, false, false), new Rune ('%') };
yield return new object [] { new ConsoleKeyInfo ('5', ConsoleKey.None, false, false, false), new Rune ('5') };
yield return new object [] { new ConsoleKeyInfo ('$', ConsoleKey.None, false, false, false), new Rune ('$') };
yield return new object [] { new ConsoleKeyInfo ('4', ConsoleKey.None, false, false, false), new Rune ('4') };
yield return new object [] { new ConsoleKeyInfo ('#', ConsoleKey.None, false, false, false), new Rune ('#') };
yield return new object [] { new ConsoleKeyInfo ('@', ConsoleKey.None, false, false, false), new Rune ('@') };
yield return new object [] { new ConsoleKeyInfo ('2', ConsoleKey.None, false, false, false), new Rune ('2') };
yield return new object [] { new ConsoleKeyInfo ('!', ConsoleKey.None, false, false, false), new Rune ('!') };
yield return new object [] { new ConsoleKeyInfo ('1', ConsoleKey.None, false, false, false), new Rune ('1') };
yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), new Rune ('\t') };
yield return new object [] { new ConsoleKeyInfo ('}', ConsoleKey.Oem6, true, false, false), new Rune ('}') };
yield return new object [] { new ConsoleKeyInfo (']', ConsoleKey.Oem6, false, false, false), new Rune (']') };
yield return new object [] { new ConsoleKeyInfo ('{', ConsoleKey.Oem4, true, false, false), new Rune ('{') };
yield return new object [] { new ConsoleKeyInfo ('[', ConsoleKey.Oem4, false, false, false), new Rune ('[') };
yield return new object [] { new ConsoleKeyInfo ('\"', ConsoleKey.Oem7, true, false, false), new Rune ('\"') };
yield return new object [] { new ConsoleKeyInfo ('\'', ConsoleKey.Oem7, false, false, false), new Rune ('\'') };
yield return new object [] { new ConsoleKeyInfo (':', ConsoleKey.Oem1, true, false, false), new Rune (':') };
yield return new object [] { new ConsoleKeyInfo (';', ConsoleKey.Oem1, false, false, false), new Rune (';') };
yield return new object [] { new ConsoleKeyInfo ('?', ConsoleKey.Oem2, true, false, false), new Rune ('?') };
yield return new object [] { new ConsoleKeyInfo ('/', ConsoleKey.Oem2, false, false, false), new Rune ('/') };
yield return new object [] { new ConsoleKeyInfo ('>', ConsoleKey.OemPeriod, true, false, false), new Rune ('>') };
yield return new object [] { new ConsoleKeyInfo ('.', ConsoleKey.OemPeriod, false, false, false), new Rune ('.') };
yield return new object [] { new ConsoleKeyInfo ('<', ConsoleKey.OemComma, true, false, false), new Rune ('<') };
yield return new object [] { new ConsoleKeyInfo (',', ConsoleKey.OemComma, false, false, false), new Rune (',') };
yield return new object [] { new ConsoleKeyInfo ('w', ConsoleKey.None, false, false, false), new Rune ('w') };
yield return new object [] { new ConsoleKeyInfo ('e', ConsoleKey.None, false, false, false), new Rune ('e') };
yield return new object [] { new ConsoleKeyInfo ('a', ConsoleKey.None, false, false, false), new Rune ('a') };
yield return new object [] { new ConsoleKeyInfo ('s', ConsoleKey.None, false, false, false), new Rune ('s') };
}
[Theory]
[MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Rune))]
public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune expected)
{
var converter = new NetKeyConverter ();
// Act
var result = converter.ToKey (input);
// Assert
Assert.Equal (expected, result.AsRune);
}
public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key ()
{
yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), Key.Tab };
yield return new object [] { new ConsoleKeyInfo ('\u001B', ConsoleKey.None, false, false, false), Key.Esc };
yield return new object [] { new ConsoleKeyInfo ('\u007f', ConsoleKey.None, false, false, false), Key.Backspace };
// TODO: Terminal.Gui does not have a Key for this mapped
// TODO: null and default(Key) are both not same as Null. Why user has to do (Key)0 to get a null key?!
yield return new object [] { new ConsoleKeyInfo ('\0', ConsoleKey.LeftWindows, false, false, false), (Key)0 };
}
[Theory]
[MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Key))]
public void ConsoleKeyInfoToKey_ValidInput_AsKey (ConsoleKeyInfo input, Key expected)
{
var converter = new NetKeyConverter ();
// Act
var result = converter.ToKey (input);
// Assert
Assert.Equal (expected, result);
}
[Fact]
public void Test_ProcessQueue_CapitalHLowerE ()
{
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
queue.Enqueue (new ('H', ConsoleKey.None, true, false, false));
queue.Enqueue (new ('e', ConsoleKey.None, false, false, false));
var processor = new NetInputProcessor (queue);
List<Key> ups = new ();
List<Key> downs = new ();
processor.KeyUp += (s, e) => { ups.Add (e); };
processor.KeyDown += (s, e) => { downs.Add (e); };
Assert.Empty (ups);
Assert.Empty (downs);
processor.ProcessQueue ();
Assert.Equal (Key.H.WithShift, ups [0]);
Assert.Equal (Key.H.WithShift, downs [0]);
Assert.Equal (Key.E, ups [1]);
Assert.Equal (Key.E, downs [1]);
}
}

View File

@@ -2,7 +2,7 @@
using System.Text;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ConoleDriverTests;
namespace UnitTests_Parallelizable.DriverTests;
public class Osc8UrlLinkerTests (ITestOutputHelper output)
{

View File

@@ -0,0 +1,75 @@
using Moq;
namespace UnitTests_Parallelizable.DriverTests;
public class WindowSizeMonitorTests
{
[Fact]
public void TestWindowSizeMonitor_RaisesEventWhenChanges ()
{
Mock<IOutput> consoleOutput = new ();
Queue<Size> queue = new (
[
new (30, 20),
new (20, 20)
]);
consoleOutput.Setup (m => m.GetSize ())
.Returns (queue.Dequeue);
var outputBuffer = Mock.Of<IOutputBuffer> ();
var monitor = new SizeMonitorImpl (consoleOutput.Object);
List<SizeChangedEventArgs> result = [];
monitor.SizeChanged += (s, e) => { result.Add (e); };
Assert.Empty (result);
monitor.Poll ();
Assert.Single (result);
Assert.Equal (new Size (30, 20), result [0].Size);
monitor.Poll ();
Assert.Equal (2, result.Count);
Assert.Equal (new Size (30, 20), result [0].Size);
Assert.Equal (new Size (20, 20), result [1].Size);
}
[Fact]
public void TestWindowSizeMonitor_DoesNotRaiseEventWhen_NoChanges ()
{
Mock<IOutput> consoleOutput = new ();
Queue<Size> queue = new (
[
new (30, 20),
new (30, 20)
]);
consoleOutput.Setup (m => m.GetSize ())
.Returns (queue.Dequeue);
var outputBuffer = Mock.Of<IOutputBuffer> ();
var monitor = new SizeMonitorImpl (consoleOutput.Object);
List<SizeChangedEventArgs> result = [];
monitor.SizeChanged += (s, e) => { result.Add (e); };
// First poll always raises event because going from unknown size i.e. 0,0
Assert.Empty (result);
monitor.Poll ();
Assert.Single (result);
Assert.Equal (new Size (30, 20), result [0].Size);
// No change
monitor.Poll ();
Assert.Single (result);
Assert.Equal (new Size (30, 20), result [0].Size);
}
}

View File

@@ -0,0 +1,492 @@
using System.Collections.Concurrent;
using InputRecord = Terminal.Gui.Drivers.WindowsConsole.InputRecord;
using ButtonState = Terminal.Gui.Drivers.WindowsConsole.ButtonState;
using EventFlags = Terminal.Gui.Drivers.WindowsConsole.EventFlags;
using ControlKeyState = Terminal.Gui.Drivers.WindowsConsole.ControlKeyState;
using MouseEventRecord = Terminal.Gui.Drivers.WindowsConsole.MouseEventRecord;
namespace UnitTests_Parallelizable.DriverTests;
public class WindowsInputProcessorTests
{
[Fact]
public void Test_ProcessQueue_CapitalHLowerE ()
{
ConcurrentQueue<InputRecord> queue = new ();
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = true,
UnicodeChar = 'H',
dwControlKeyState = ControlKeyState.CapslockOn,
wVirtualKeyCode = (VK)72,
wVirtualScanCode = 35
}
});
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = false,
UnicodeChar = 'H',
dwControlKeyState = ControlKeyState.CapslockOn,
wVirtualKeyCode = (VK)72,
wVirtualScanCode = 35
}
});
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = true,
UnicodeChar = 'i',
dwControlKeyState = ControlKeyState.NoControlKeyPressed,
wVirtualKeyCode = (VK)73,
wVirtualScanCode = 23
}
});
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Key,
KeyEvent = new ()
{
bKeyDown = false,
UnicodeChar = 'i',
dwControlKeyState = ControlKeyState.NoControlKeyPressed,
wVirtualKeyCode = (VK)73,
wVirtualScanCode = 23
}
});
var processor = new WindowsInputProcessor (queue);
List<Key> ups = [];
List<Key> downs = [];
processor.KeyUp += (s, e) => { ups.Add (e); };
processor.KeyDown += (s, e) => { downs.Add (e); };
Assert.Empty (ups);
Assert.Empty (downs);
processor.ProcessQueue ();
Assert.Equal (Key.H.WithShift, ups [0]);
Assert.Equal (Key.H.WithShift, downs [0]);
Assert.Equal (Key.I, ups [1]);
Assert.Equal (Key.I, downs [1]);
}
[Fact]
public void Test_ProcessQueue_Mouse_Move ()
{
ConcurrentQueue<InputRecord> queue = new ();
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Mouse,
MouseEvent = new ()
{
MousePosition = new (32, 31),
ButtonState = ButtonState.NoButtonPressed,
ControlKeyState = ControlKeyState.NoControlKeyPressed,
EventFlags = EventFlags.MouseMoved
}
});
var processor = new WindowsInputProcessor (queue);
List<MouseEventArgs> mouseEvents = [];
processor.MouseEvent += (s, e) => { mouseEvents.Add (e); };
Assert.Empty (mouseEvents);
processor.ProcessQueue ();
MouseEventArgs s = Assert.Single (mouseEvents);
Assert.Equal (MouseFlags.ReportMousePosition, s.Flags);
Assert.Equal (s.ScreenPosition, new (32, 31));
}
[Theory]
[InlineData (ButtonState.Button1Pressed, MouseFlags.Button1Pressed)]
[InlineData (ButtonState.Button2Pressed, MouseFlags.Button2Pressed)]
[InlineData (ButtonState.Button3Pressed, MouseFlags.Button3Pressed)]
[InlineData (ButtonState.Button4Pressed, MouseFlags.Button4Pressed)]
internal void Test_ProcessQueue_Mouse_Pressed (ButtonState state, MouseFlags expectedFlag)
{
ConcurrentQueue<InputRecord> queue = new ();
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Mouse,
MouseEvent = new ()
{
MousePosition = new (32, 31),
ButtonState = state,
ControlKeyState = ControlKeyState.NoControlKeyPressed,
EventFlags = EventFlags.MouseMoved
}
});
var processor = new WindowsInputProcessor (queue);
List<MouseEventArgs> mouseEvents = [];
processor.MouseEvent += (s, e) => { mouseEvents.Add (e); };
Assert.Empty (mouseEvents);
processor.ProcessQueue ();
MouseEventArgs s = Assert.Single (mouseEvents);
Assert.Equal (s.Flags, MouseFlags.ReportMousePosition | expectedFlag);
Assert.Equal (s.ScreenPosition, new (32, 31));
}
[Theory]
[InlineData (100, MouseFlags.WheeledUp)]
[InlineData (-100, MouseFlags.WheeledDown)]
internal void Test_ProcessQueue_Mouse_Wheel (int wheelValue, MouseFlags expectedFlag)
{
ConcurrentQueue<InputRecord> queue = new ();
queue.Enqueue (
new ()
{
EventType = WindowsConsole.EventType.Mouse,
MouseEvent = new ()
{
MousePosition = new (32, 31),
ButtonState = (ButtonState)wheelValue,
ControlKeyState = ControlKeyState.NoControlKeyPressed,
EventFlags = EventFlags.MouseWheeled
}
});
var processor = new WindowsInputProcessor (queue);
List<MouseEventArgs> mouseEvents = [];
processor.MouseEvent += (s, e) => { mouseEvents.Add (e); };
Assert.Empty (mouseEvents);
processor.ProcessQueue ();
MouseEventArgs s = Assert.Single (mouseEvents);
Assert.Equal (s.Flags, expectedFlag);
Assert.Equal (s.ScreenPosition, new (32, 31));
}
public static IEnumerable<object []> MouseFlagTestData ()
{
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button1Pressed),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button1Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button2Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button2Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button3Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button3Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button4Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button4Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.MouseMoved, ControlKeyState.NoControlKeyPressed, MouseFlags.ReportMousePosition)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.RightmostButtonPressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button3Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
// Tests for holding down 2 buttons at once and releasing them one after the other
yield return
[
new []
{
Tuple.Create (
ButtonState.Button1Pressed | ButtonState.Button2Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button1Pressed | MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (
ButtonState.Button1Pressed,
EventFlags.NoEvent,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button1Pressed | MouseFlags.Button2Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button1Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button3Pressed | ButtonState.Button4Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (
ButtonState.Button3Pressed,
EventFlags.NoEvent,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.Button4Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button3Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
// Test for holding down 2 buttons at once and releasing them simultaneously
yield return
[
new []
{
Tuple.Create (
ButtonState.Button1Pressed | ButtonState.Button2Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button1Pressed | MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (
ButtonState.NoButtonPressed,
EventFlags.NoEvent,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button1Released | MouseFlags.Button2Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
// Test that rightmost and button 3 are the same button so 2 states is still only 1 flag
yield return
[
new []
{
Tuple.Create (
ButtonState.Button3Pressed | ButtonState.RightmostButtonPressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
// Can swap between without raising the released
Tuple.Create (
ButtonState.Button3Pressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
Tuple.Create (
ButtonState.RightmostButtonPressed,
EventFlags.MouseMoved,
ControlKeyState.NoControlKeyPressed,
MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition),
// Now with neither we get released
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.Button3Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NoControlKeyPressed, MouseFlags.None)
}
];
// Test for ControlKeyState buttons pressed and handled
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.LeftAltPressed, MouseFlags.Button1Pressed | MouseFlags.ButtonAlt),
Tuple.Create (
ButtonState.NoButtonPressed,
EventFlags.NoEvent,
ControlKeyState.LeftAltPressed,
MouseFlags.Button1Released | MouseFlags.ButtonAlt),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.LeftAltPressed, MouseFlags.None | MouseFlags.ButtonAlt)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button1Pressed,
EventFlags.NoEvent,
ControlKeyState.RightAltPressed,
MouseFlags.Button1Pressed | MouseFlags.ButtonAlt),
Tuple.Create (
ButtonState.NoButtonPressed,
EventFlags.NoEvent,
ControlKeyState.RightAltPressed,
MouseFlags.Button1Released | MouseFlags.ButtonAlt),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.RightAltPressed, MouseFlags.None | MouseFlags.ButtonAlt)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button1Pressed,
EventFlags.NoEvent,
ControlKeyState.LeftControlPressed,
MouseFlags.Button1Pressed | MouseFlags.ButtonCtrl),
Tuple.Create (
ButtonState.NoButtonPressed,
EventFlags.NoEvent,
ControlKeyState.LeftControlPressed,
MouseFlags.Button1Released | MouseFlags.ButtonCtrl),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.LeftControlPressed, MouseFlags.None | MouseFlags.ButtonCtrl)
}
];
yield return
[
new []
{
Tuple.Create (
ButtonState.Button1Pressed,
EventFlags.NoEvent,
ControlKeyState.RightControlPressed,
MouseFlags.Button1Pressed | MouseFlags.ButtonCtrl),
Tuple.Create (
ButtonState.NoButtonPressed,
EventFlags.NoEvent,
ControlKeyState.RightControlPressed,
MouseFlags.Button1Released | MouseFlags.ButtonCtrl),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.RightControlPressed, MouseFlags.None | MouseFlags.ButtonCtrl)
}
];
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.ShiftPressed, MouseFlags.Button1Pressed | MouseFlags.ButtonShift),
Tuple.Create (
ButtonState.NoButtonPressed,
EventFlags.NoEvent,
ControlKeyState.ShiftPressed,
MouseFlags.Button1Released | MouseFlags.ButtonShift),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.ShiftPressed, MouseFlags.None | MouseFlags.ButtonShift)
}
];
// Test for ControlKeyState buttons pressed and not handled
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.CapslockOn, MouseFlags.Button1Pressed),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.CapslockOn, MouseFlags.Button1Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.CapslockOn, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.EnhancedKey, MouseFlags.Button1Pressed),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.EnhancedKey, MouseFlags.Button1Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.EnhancedKey, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.NumlockOn, MouseFlags.Button1Pressed),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NumlockOn, MouseFlags.Button1Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.NumlockOn, MouseFlags.None)
}
];
yield return
[
new []
{
Tuple.Create (ButtonState.Button1Pressed, EventFlags.NoEvent, ControlKeyState.ScrolllockOn, MouseFlags.Button1Pressed),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.ScrolllockOn, MouseFlags.Button1Released),
Tuple.Create (ButtonState.NoButtonPressed, EventFlags.NoEvent, ControlKeyState.ScrolllockOn, MouseFlags.None)
}
];
}
[Theory]
[MemberData (nameof (MouseFlagTestData))]
internal void MouseFlags_Should_Map_Correctly (Tuple<ButtonState, EventFlags, ControlKeyState, MouseFlags> [] inputOutputPairs)
{
var processor = new WindowsInputProcessor (new ());
foreach (Tuple<ButtonState, EventFlags, ControlKeyState, MouseFlags> pair in inputOutputPairs)
{
var mockEvent = new MouseEventRecord { ButtonState = pair.Item1, EventFlags = pair.Item2, ControlKeyState = pair.Item3 };
MouseEventArgs result = processor.ToMouseEvent (mockEvent);
Assert.Equal (pair.Item4, result.Flags);
}
}
}

View File

@@ -0,0 +1,365 @@
#nullable enable
using System.Collections.Concurrent;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
/// <summary>
/// Parallelizable unit tests for IInput.EnqueueKeyDownEvent and InputProcessor.EnqueueKeyDownEvent.
/// Tests validate the entire pipeline: Key → TInputRecord → Queue → ProcessQueue → Events.
/// </summary>
[Trait ("Category", "Input")]
public class EnqueueKeyEventTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
#region Helper Methods
/// <summary>
/// Simulates the input thread by manually draining FakeInput's internal queue
/// and moving items to the InputBuffer. This is needed because tests don't
/// start the actual input thread via Run().
/// </summary>
private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
{
// FakeInput's Peek() checks _testInput
while (fakeInput.Peek ())
{
// Read() drains _testInput and returns items
foreach (ConsoleKeyInfo item in fakeInput.Read ())
{
// Manually add to InputBuffer (simulating what Run() would do)
inputBuffer.Enqueue (item);
}
}
}
/// <summary>
/// Processes the input queue with support for keys that may be held by the ANSI parser (like Esc).
/// The parser holds Esc for 50ms waiting to see if it's part of an escape sequence.
/// </summary>
private static void ProcessQueueWithEscapeHandling (FakeInputProcessor processor, int maxAttempts = 3)
{
// First attempt - process immediately
processor.ProcessQueue ();
// For escape sequences, we may need to wait and process again
// The parser holds escape for 50ms before releasing
for (var attempt = 1; attempt < maxAttempts; attempt++)
{
Thread.Sleep (60); // Wait longer than the 50ms escape timeout
processor.ProcessQueue (); // This should release any held escape keys
}
}
#endregion
#region FakeInput EnqueueKeyDownEvent Tests
[Fact]
public void FakeInput_EnqueueKeyDownEvent_AddsSingleKeyToQueue ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
List<Key> receivedKeys = [];
processor.KeyDown += (_, k) => receivedKeys.Add (k);
Key key = Key.A;
// Act
processor.EnqueueKeyDownEvent (key);
// Simulate the input thread moving items from _testInput to InputBuffer
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert - Verify the key made it through
Assert.Single (receivedKeys);
Assert.Equal (key, receivedKeys [0]);
}
[Fact]
public void FakeInput_EnqueueKeyDownEvent_SupportsMultipleKeys ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
Key [] keys = [Key.A, Key.B, Key.C, Key.Enter];
List<Key> receivedKeys = [];
processor.KeyDown += (_, k) => receivedKeys.Add (k);
// Act
foreach (Key key in keys)
{
processor.EnqueueKeyDownEvent (key);
}
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.Equal (keys.Length, receivedKeys.Count);
Assert.Equal (keys, receivedKeys);
}
[Theory]
[InlineData (KeyCode.A, false, false, false)]
[InlineData (KeyCode.A, true, false, false)] // Shift+A
[InlineData (KeyCode.A, false, true, false)] // Ctrl+A
[InlineData (KeyCode.A, false, false, true)] // Alt+A
[InlineData (KeyCode.A, true, true, true)] // Ctrl+Shift+Alt+A
public void FakeInput_EnqueueKeyDownEvent_PreservesModifiers (KeyCode keyCode, bool shift, bool ctrl, bool alt)
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
var key = new Key (keyCode);
if (shift)
{
key = key.WithShift;
}
if (ctrl)
{
key = key.WithCtrl;
}
if (alt)
{
key = key.WithAlt;
}
Key? receivedKey = null;
processor.KeyDown += (_, k) => receivedKey = k;
// Act
processor.EnqueueKeyDownEvent (key);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.NotNull (receivedKey);
Assert.Equal (key.IsShift, receivedKey.IsShift);
Assert.Equal (key.IsCtrl, receivedKey.IsCtrl);
Assert.Equal (key.IsAlt, receivedKey.IsAlt);
Assert.Equal (key.KeyCode, receivedKey.KeyCode);
}
[Theory]
[InlineData (KeyCode.Enter)]
[InlineData (KeyCode.Tab)]
[InlineData (KeyCode.Esc)]
[InlineData (KeyCode.Backspace)]
[InlineData (KeyCode.Delete)]
[InlineData (KeyCode.CursorUp)]
[InlineData (KeyCode.CursorDown)]
[InlineData (KeyCode.CursorLeft)]
[InlineData (KeyCode.CursorRight)]
[InlineData (KeyCode.F1)]
[InlineData (KeyCode.F12)]
public void FakeInput_EnqueueKeyDownEvent_SupportsSpecialKeys (KeyCode keyCode)
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
var key = new Key (keyCode);
Key? receivedKey = null;
processor.KeyDown += (_, k) => receivedKey = k;
// Act
processor.EnqueueKeyDownEvent (key);
SimulateInputThread (fakeInput, queue);
// Esc is special - the ANSI parser holds it waiting for potential escape sequences
// We need to process with delay to let the parser release it after timeout
if (keyCode == KeyCode.Esc)
{
ProcessQueueWithEscapeHandling (processor);
}
else
{
processor.ProcessQueue ();
}
// Assert
Assert.NotNull (receivedKey);
Assert.Equal (key.KeyCode, receivedKey.KeyCode);
}
[Fact]
public void FakeInput_EnqueueKeyDownEvent_RaisesKeyDownAndKeyUpEvents ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
var keyDownCount = 0;
var keyUpCount = 0;
processor.KeyDown += (_, _) => keyDownCount++;
processor.KeyUp += (_, _) => keyUpCount++;
// Act
processor.EnqueueKeyDownEvent (Key.A);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert - FakeDriver simulates KeyUp immediately after KeyDown
Assert.Equal (1, keyDownCount);
Assert.Equal (1, keyUpCount);
}
#endregion
#region InputProcessor Pipeline Tests
[Fact]
public void InputProcessor_EnqueueKeyDownEvent_RequiresTestableInput ()
{
// Arrange
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
var processor = new FakeInputProcessor (queue);
// Don't set InputImpl (or set to non-testable)
// Act & Assert - Should not throw, but also won't add to queue
// (because InputImpl is null or not ITestableInput)
processor.EnqueueKeyDownEvent (Key.A);
processor.ProcessQueue ();
// No events should be raised since no input was added
var eventRaised = false;
processor.KeyDown += (_, _) => eventRaised = true;
processor.ProcessQueue ();
Assert.False (eventRaised);
}
[Fact]
public void InputProcessor_ProcessQueue_DrainsPendingInputRecords ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
List<Key> receivedKeys = [];
processor.KeyDown += (_, k) => receivedKeys.Add (k);
// Act - Enqueue multiple keys before processing
processor.EnqueueKeyDownEvent (Key.A);
processor.EnqueueKeyDownEvent (Key.B);
processor.EnqueueKeyDownEvent (Key.C);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert - After processing, queue should be empty and all keys received
Assert.Empty (queue);
Assert.Equal (3, receivedKeys.Count);
}
#endregion
#region Thread Safety Tests
[Fact]
public void FakeInput_EnqueueKeyDownEvent_IsThreadSafe ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
ConcurrentBag<Key> receivedKeys = [];
processor.KeyDown += (_, k) => receivedKeys.Add (k);
const int threadCount = 10;
const int keysPerThread = 100;
Thread [] threads = new Thread [threadCount];
// Act - Enqueue keys from multiple threads
for (var t = 0; t < threadCount; t++)
{
threads [t] = new (() =>
{
for (var i = 0; i < keysPerThread; i++)
{
processor.EnqueueKeyDownEvent (Key.A);
}
});
threads [t].Start ();
}
// Wait for all threads to complete
foreach (Thread thread in threads)
{
thread.Join ();
}
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.Equal (threadCount * keysPerThread, receivedKeys.Count);
}
#endregion
#region Error Handling Tests
[Fact]
public void FakeInput_EnqueueKeyDownEvent_WithInvalidKey_DoesNotThrow ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
// Act & Assert - Empty/null key should not throw
Exception? exception = Record.Exception (() =>
{
processor.EnqueueKeyDownEvent (Key.Empty);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
});
Assert.Null (exception);
}
#endregion
}

View File

@@ -0,0 +1,531 @@
#nullable enable
using System.Collections.Concurrent;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.DriverTests;
/// <summary>
/// Parallelizable unit tests for IInputProcessor.EnqueueMouseEvent.
/// Tests validate the entire pipeline: MouseEventArgs → TInputRecord → Queue → ProcessQueue → Events.
/// fully implemented in InputProcessorImpl (base class). Only WindowsInputProcessor has a working implementation.
/// </summary>
[Trait ("Category", "Input")]
public class EnqueueMouseEventTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
#region Mouse Event Sequencing Tests
[Fact]
public void FakeInput_EnqueueMouseEvent_HandlesCompleteClickSequence ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
List<MouseEventArgs> receivedEvents = [];
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
// Act - Simulate a complete click: press → release → click
processor.EnqueueMouseEvent (
new()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Pressed
});
processor.EnqueueMouseEvent (
new()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Released
});
// The MouseInterpreter in the processor should generate a clicked event
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
// We should see at least the pressed and released events
Assert.True (receivedEvents.Count >= 2);
Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Pressed));
Assert.Contains (receivedEvents, e => e.Flags.HasFlag (MouseFlags.Button1Released));
}
#endregion
#region Thread Safety Tests
[Fact]
public void FakeInput_EnqueueMouseEvent_IsThreadSafe ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
ConcurrentBag<MouseEventArgs> receivedEvents = [];
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
const int threadCount = 10;
const int eventsPerThread = 100;
Thread [] threads = new Thread [threadCount];
// Act - Enqueue mouse events from multiple threads
for (var t = 0; t < threadCount; t++)
{
int threadId = t;
threads [t] = new (() =>
{
for (var i = 0; i < eventsPerThread; i++)
{
processor.EnqueueMouseEvent (
new()
{
Position = new (threadId, i),
Flags = MouseFlags.Button1Clicked
});
}
});
threads [t].Start ();
}
// Wait for all threads to complete
foreach (Thread thread in threads)
{
thread.Join ();
}
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.Equal (threadCount * eventsPerThread, receivedEvents.Count);
}
#endregion
#region Helper Methods
/// <summary>
/// Simulates the input thread by manually draining FakeInput's internal queue
/// and moving items to the InputBuffer. This is needed because tests don't
/// start the actual input thread via Run().
/// </summary>
private static void SimulateInputThread (FakeInput fakeInput, ConcurrentQueue<ConsoleKeyInfo> inputBuffer)
{
// FakeInput's Peek() checks _testInput
while (fakeInput.Peek ())
{
// Read() drains _testInput and returns items
foreach (ConsoleKeyInfo item in fakeInput.Read ())
{
// Manually add to InputBuffer (simulating what Run() would do)
inputBuffer.Enqueue (item);
}
}
}
#endregion
#region FakeInputProcessor EnqueueMouseEvent Tests
[Fact]
public void FakeInput_EnqueueMouseEvent_AddsSingleMouseEventToQueue ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
List<MouseEventArgs> receivedEvents = [];
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
MouseEventArgs mouseEvent = new ()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Clicked
};
// Act
processor.EnqueueMouseEvent (mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert - Verify the mouse event made it through
Assert.Single (receivedEvents);
Assert.Equal (mouseEvent.Position, receivedEvents [0].Position);
Assert.Equal (mouseEvent.Flags, receivedEvents [0].Flags);
}
[Fact]
public void FakeInput_EnqueueMouseEvent_SupportsMultipleEvents ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
MouseEventArgs [] events =
[
new () { Position = new (10, 5), Flags = MouseFlags.Button1Pressed },
new () { Position = new (10, 5), Flags = MouseFlags.Button1Released },
new () { Position = new (15, 8), Flags = MouseFlags.ReportMousePosition },
new () { Position = new (20, 10), Flags = MouseFlags.Button1Clicked }
];
List<MouseEventArgs> receivedEvents = [];
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
// Act
foreach (MouseEventArgs mouseEvent in events)
{
processor.EnqueueMouseEvent (mouseEvent);
}
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
// The MouseInterpreter processes Button1Pressed followed by Button1Released and generates
// an additional Button1Clicked event, so we expect 5 events total:
// 1. Button1Pressed (original)
// 2. Button1Released (original)
// 3. Button1Clicked (generated by MouseInterpreter from press+release)
// 4. ReportMousePosition (original)
// 5. Button1Clicked (original)
Assert.Equal (5, receivedEvents.Count);
// Verify the original events are present
Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Pressed && e.Position == new Point (10, 5));
Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.Button1Released && e.Position == new Point (10, 5));
Assert.Contains (receivedEvents, e => e.Flags == MouseFlags.ReportMousePosition && e.Position == new Point (15, 8));
// There should be two clicked events: one generated, one original
var clickedEvents = receivedEvents.Where (e => e.Flags == MouseFlags.Button1Clicked).ToList ();
Assert.Equal (2, clickedEvents.Count);
Assert.Contains (clickedEvents, e => e.Position == new Point (10, 5)); // Generated from press+release
Assert.Contains (clickedEvents, e => e.Position == new Point (20, 10)); // Original
}
[Theory]
[InlineData (MouseFlags.Button1Clicked)]
[InlineData (MouseFlags.Button2Clicked)]
[InlineData (MouseFlags.Button3Clicked)]
[InlineData (MouseFlags.Button4Clicked)]
[InlineData (MouseFlags.Button1DoubleClicked)]
[InlineData (MouseFlags.Button1TripleClicked)]
public void FakeInput_EnqueueMouseEvent_SupportsAllButtonClicks (MouseFlags flags)
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
MouseEventArgs mouseEvent = new ()
{
Position = new (10, 5),
Flags = flags
};
MouseEventArgs? receivedEvent = null;
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.NotNull (receivedEvent);
Assert.Equal (flags, receivedEvent.Flags);
}
[Theory]
[InlineData (0, 0)]
[InlineData (10, 5)]
[InlineData (79, 24)] // Near screen edge (assuming 80x25)
[InlineData (100, 100)] // Beyond typical screen
public void FakeInput_EnqueueMouseEvent_PreservesPosition (int x, int y)
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
MouseEventArgs mouseEvent = new ()
{
Position = new (x, y),
Flags = MouseFlags.Button1Clicked
};
MouseEventArgs? receivedEvent = null;
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.NotNull (receivedEvent);
Assert.Equal (x, receivedEvent.Position.X);
Assert.Equal (y, receivedEvent.Position.Y);
}
[Theory]
[InlineData (MouseFlags.ButtonShift)]
[InlineData (MouseFlags.ButtonCtrl)]
[InlineData (MouseFlags.ButtonAlt)]
[InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl)]
[InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonAlt)]
[InlineData (MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
[InlineData (MouseFlags.ButtonShift | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt)]
public void FakeInput_EnqueueMouseEvent_PreservesModifiers (MouseFlags modifiers)
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
MouseEventArgs mouseEvent = new ()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Clicked | modifiers
};
MouseEventArgs? receivedEvent = null;
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.NotNull (receivedEvent);
Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.Button1Clicked));
if (modifiers.HasFlag (MouseFlags.ButtonShift))
{
Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonShift));
}
if (modifiers.HasFlag (MouseFlags.ButtonCtrl))
{
Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonCtrl));
}
if (modifiers.HasFlag (MouseFlags.ButtonAlt))
{
Assert.True (receivedEvent.Flags.HasFlag (MouseFlags.ButtonAlt));
}
}
[Theory]
[InlineData (MouseFlags.WheeledUp)]
[InlineData (MouseFlags.WheeledDown)]
[InlineData (MouseFlags.WheeledLeft)]
[InlineData (MouseFlags.WheeledRight)]
public void FakeInput_EnqueueMouseEvent_SupportsMouseWheel (MouseFlags wheelFlag)
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
MouseEventArgs mouseEvent = new ()
{
Position = new (10, 5),
Flags = wheelFlag
};
MouseEventArgs? receivedEvent = null;
processor.MouseEvent += (_, e) => receivedEvent = e;
// Act
processor.EnqueueMouseEvent (mouseEvent);
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.NotNull (receivedEvent);
Assert.True (receivedEvent.Flags.HasFlag (wheelFlag));
}
[Fact]
public void FakeInput_EnqueueMouseEvent_SupportsMouseMove ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
List<MouseEventArgs> receivedEvents = [];
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
MouseEventArgs [] events =
[
new () { Position = new (0, 0), Flags = MouseFlags.ReportMousePosition },
new () { Position = new (5, 5), Flags = MouseFlags.ReportMousePosition },
new () { Position = new (10, 10), Flags = MouseFlags.ReportMousePosition }
];
// Act
foreach (MouseEventArgs mouseEvent in events)
{
processor.EnqueueMouseEvent (mouseEvent);
}
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert
Assert.Equal (3, receivedEvents.Count);
Assert.Equal (new (0, 0), receivedEvents [0].Position);
Assert.Equal (new (5, 5), receivedEvents [1].Position);
Assert.Equal (new (10, 10), receivedEvents [2].Position);
}
#endregion
#region InputProcessor Pipeline Tests
[Fact]
public void InputProcessor_EnqueueMouseEvent_DoesNotThrow ()
{
// Arrange
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
var processor = new FakeInputProcessor (queue);
// Don't set InputImpl (or set to non-testable)
// Act & Assert - Should not throw even if not implemented
Exception? exception = Record.Exception (() =>
{
processor.EnqueueMouseEvent (
new()
{
Position = new (10, 5),
Flags = MouseFlags.Button1Clicked
});
processor.ProcessQueue ();
});
// The base implementation logs a critical message but doesn't throw
Assert.Null (exception);
}
[Fact]
public void InputProcessor_ProcessQueue_DrainsPendingMouseEvents ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
List<MouseEventArgs> receivedEvents = [];
processor.MouseEvent += (_, e) => receivedEvents.Add (e);
// Act - Enqueue multiple events before processing
processor.EnqueueMouseEvent (new() { Position = new (1, 1), Flags = MouseFlags.Button1Pressed });
processor.EnqueueMouseEvent (new() { Position = new (2, 2), Flags = MouseFlags.ReportMousePosition });
processor.EnqueueMouseEvent (new() { Position = new (3, 3), Flags = MouseFlags.Button1Released });
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
// Assert - After processing, all events should be received
Assert.Empty (queue);
Assert.Equal (3, receivedEvents.Count);
}
#endregion
#region Error Handling Tests
[Fact]
public void FakeInput_EnqueueMouseEvent_WithInvalidEvent_DoesNotThrow ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
// Act & Assert - Empty/default mouse event should not throw
Exception? exception = Record.Exception (() =>
{
processor.EnqueueMouseEvent (new ());
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
});
Assert.Null (exception);
}
[Fact]
public void FakeInput_EnqueueMouseEvent_WithNegativePosition_DoesNotThrow ()
{
// Arrange
var fakeInput = new FakeInput ();
ConcurrentQueue<ConsoleKeyInfo> queue = new ();
fakeInput.Initialize (queue);
var processor = new FakeInputProcessor (queue);
processor.InputImpl = fakeInput;
// Act & Assert - Negative positions should not throw
Exception? exception = Record.Exception (() =>
{
processor.EnqueueMouseEvent (
new()
{
Position = new (-10, -5),
Flags = MouseFlags.Button1Clicked
});
SimulateInputThread (fakeInput, queue);
processor.ProcessQueue ();
});
Assert.Null (exception);
}
#endregion
}

View File

@@ -591,4 +591,20 @@ public class KeyTests
r.Dispose ();
}
[Fact]
public void Set_Key_Separator_With_Rune_Default_Ensure_Using_The_Default_Plus ()
{
Key key = new (Key.A.WithCtrl);
Assert.Equal ((Rune)'+', Key.Separator);
Assert.Equal ("Ctrl+A", key.ToString ());
// NOTE: This means this test can't be parallelized
Key.Separator = new ('-');
Assert.Equal ((Rune)'-', Key.Separator);
Assert.Equal ("Ctrl-A", key.ToString ());
Key.Separator = new ();
Assert.Equal ((Rune)'+', Key.Separator);
Assert.Equal ("Ctrl+A", key.ToString ());
}
}

View File

@@ -1,29 +0,0 @@
using TerminalGuiFluentTesting;
namespace UnitTests.Parallelizable;
/// <summary>
/// Base class for parallelizable tests. Ensures that tests can run in parallel without interference
/// by setting various Terminal.Gui static properties to their default values. E.g. View.EnableDebugIDisposableAsserts.
/// </summary>
[Collection ("Global Test Setup")]
public abstract class ParallelizableBase
{
// Common setup or utilities for all tests can go here
/// <summary>
/// Creates a new FakeDriver instance with the specified buffer size.
/// This is a convenience method for tests that need to use Draw() and DriverAssert
/// without relying on Application.Driver.
/// </summary>
/// <param name="width">Width of the driver buffer</param>
/// <param name="height">Height of the driver buffer</param>
/// <returns>A configured IFakeConsoleDriver instance</returns>
protected static IConsoleDriver CreateFakeDriver (int width = 25, int height = 25)
{
IConsoleDriver driver = new FakeDriver ();
driver.SetScreenSize (width, height);
return driver;
}
}

View File

@@ -4,10 +4,11 @@ using System.Collections;
using System.Globalization;
using System.Resources;
using System.Runtime.CompilerServices;
using UnitTests;
namespace UnitTests_Parallelizable.ResourcesTests;
public class ResourceManagerTests : UnitTests.Parallelizable.ParallelizableBase
public class ResourceManagerTests : FakeDriverBase
{
private const string EXISTENT_CULTURE = "pt-PT";
private const string NO_EXISTENT_CULTURE = "de-DE";

View File

@@ -47,7 +47,7 @@ public class GlobalTestSetup : IDisposable
// Don't check Application.Force16Colors
//Assert.False (Application.Force16Colors);
Assert.Null (Application.Driver);
Assert.False (Application.EndAfterFirstIteration);
Assert.False (Application.StopAfterFirstIteration);
Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey);
Assert.Equal (Key.Tab, Application.NextTabKey);
Assert.Equal (Key.F6.WithShift, Application.PrevTabGroupKey);
@@ -58,8 +58,7 @@ public class GlobalTestSetup : IDisposable
Assert.False (Application.Initialized);
Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures);
Assert.Equal (Application.GetAvailableCulturesFromEmbeddedResources (), Application.SupportedCultures);
Assert.False (Application._forceFakeConsole);
Assert.Equal (-1, Application.MainThreadId);
Assert.Null (Application.MainThreadId);
Assert.Empty (Application.TopLevels);
Assert.Empty (Application.CachedViewsUnderMouse);
@@ -74,9 +73,9 @@ public class GlobalTestSetup : IDisposable
Assert.Null (Application.Popover);
// Events - Can't check
//Assert.Null (Application.NotifyNewRunState);
//Assert.Null (Application.NotifyNewRunState);
//Assert.Null (Application.Iteration);
//Assert.Null (Application.SessionBegun);
//Assert.Null (Application.SessionBegun);
//Assert.Null (ApplicationImpl.Instance.Iteration);
//Assert.Null (Application.SizeChanging);
//Assert.Null (Application.GrabbedMouse);
//Assert.Null (Application.UnGrabbingMouse);

View File

@@ -1,5 +1,3 @@
using System.Text.RegularExpressions;
using TerminalGuiFluentTesting;
using UnitTests;
using Xunit.Abstractions;
@@ -9,14 +7,10 @@ namespace UnitTests_Parallelizable.TextTests;
/// Pure unit tests for Autocomplete functionality that don't require Application or Driver.
/// Integration tests for Autocomplete (popup behavior, rendering) remain in UnitTests.
/// </summary>
public class AutocompleteTests : UnitTests.Parallelizable.ParallelizableBase
public class AutocompleteTests (ITestOutputHelper output) : FakeDriverBase
{
private readonly ITestOutputHelper _output;
public AutocompleteTests (ITestOutputHelper output)
{
_output = output;
}
private readonly ITestOutputHelper _output = output;
[Fact]
public void Test_GenerateSuggestions_Simple ()
{

View File

@@ -0,0 +1,669 @@
#nullable enable
using System.Text;
using UICatalog;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests.TextTests;
public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase
{
public static IEnumerable<object []> CMGlyphs =>
new List<object []> { new object [] { $"{Glyphs.LeftBracket} Say Hello 你 {Glyphs.RightBracket}", 16, 15 } };
[Theory]
[InlineData ("A", 1, 0, "")]
[InlineData ("A", 0, 1, "")]
[InlineData ("AB1 2", 2, 1, "2")]
[InlineData ("AB12", 5, 1, "21BA")]
[InlineData ("AB\n12", 5, 2, "21\nBA")]
[InlineData ("ABC 123 456", 7, 2, "CBA \n654 321")]
[InlineData ("こんにちは", 1, 1, "")]
[InlineData ("こんにちは", 2, 1, "は")]
[InlineData ("こんにちは", 5, 1, "はち")]
[InlineData ("こんにちは", 10, 1, "はちにんこ")]
[InlineData ("こんにちは\nAB\n12", 10, 3, "21 \nBA \nはちにんこ")]
public void Draw_Horizontal_RightLeft_BottomTop (string text, int width, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.RightLeft_BottomTop
};
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
[InlineData ("A", 1, 0, "")]
[InlineData ("A", 0, 1, "")]
[InlineData ("AB1 2", 2, 1, "2")]
[InlineData ("AB12", 5, 1, "21BA")]
[InlineData ("AB\n12", 5, 2, "BA\n21")]
[InlineData ("ABC 123 456", 7, 2, "654 321\nCBA ")]
[InlineData ("こんにちは", 1, 1, "")]
[InlineData ("こんにちは", 2, 1, "は")]
[InlineData ("こんにちは", 5, 1, "はち")]
[InlineData ("こんにちは", 10, 1, "はちにんこ")]
[InlineData ("こんにちは\nAB\n12", 10, 3, "はちにんこ\nBA \n21 ")]
public void Draw_Horizontal_RightLeft_TopBottom (string text, int width, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.RightLeft_TopBottom
};
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
[InlineData ("A", 0, 1, "", 0)]
[InlineData ("A", 1, 1, "A", 0)]
[InlineData ("A", 2, 2, " A", 1)]
[InlineData ("AB", 1, 1, "B", 0)]
[InlineData ("AB", 2, 2, " A\n B", 0)]
[InlineData ("ABC", 3, 2, " B\n C", 0)]
[InlineData ("ABC", 4, 2, " B\n C", 0)]
[InlineData ("ABC", 6, 2, " B\n C", 0)]
[InlineData ("こんにちは", 0, 1, "", 0)]
[InlineData ("こんにちは", 1, 0, "", 0)]
[InlineData ("こんにちは", 1, 1, "", 0)]
[InlineData ("こんにちは", 2, 1, "は", 0)]
[InlineData ("こんにちは", 2, 2, "ち\nは", 0)]
[InlineData ("こんにちは", 2, 3, "に\nち\nは", 0)]
[InlineData ("こんにちは", 2, 4, "ん\nに\nち\nは", 0)]
[InlineData ("こんにちは", 2, 5, "こ\nん\nに\nち\nは", 0)]
[InlineData ("こんにちは", 2, 6, "こ\nん\nに\nち\nは", 1)]
[InlineData ("ABCD\nこんにちは", 4, 7, " こ\n Aん\n Bに\n Cち\n Dは", 2)]
[InlineData ("こんにちは\nABCD", 3, 7, "こ \nんA\nにB\nちC\nはD", 2)]
public void Draw_Vertical_Bottom_Horizontal_Right (string text, int width, int height, string expectedText, int expectedY)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Alignment = Alignment.End,
Direction = TextDirection.TopBottom_LeftRight,
VerticalAlignment = Alignment.End
};
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (Point.Empty, new (width, height)), Attribute.Default, Attribute.Default, driver: driver);
Rectangle rect = DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
Assert.Equal (expectedY, rect.Y);
}
[Theory]
[InlineData ("A", 1, 0, "")]
[InlineData ("A", 0, 1, "")]
[InlineData ("AB1 2", 1, 2, "2")]
[InlineData ("AB12", 1, 5, "2\n1\nB\nA")]
[InlineData ("AB\n12", 2, 5, "B2\nA1")]
[InlineData ("ABC 123 456", 2, 7, "6C\n5B\n4A\n \n3 \n2 \n1 ")]
[InlineData ("こんにちは", 1, 1, "")]
[InlineData ("こんにちは", 2, 1, "は")]
[InlineData ("こんにちは", 2, 5, "は\nち\nに\nん\nこ")]
[InlineData ("こんにちは", 2, 10, "は\nち\nに\nん\nこ")]
[InlineData ("こんにちは\nAB\n12", 4, 10, "はB2\nちA1\nに \nん \nこ ")]
public void Draw_Vertical_BottomTop_LeftRight (string text, int width, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.BottomTop_LeftRight
};
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
[InlineData ("A", 1, 0, "")]
[InlineData ("A", 0, 1, "")]
[InlineData ("AB1 2", 1, 2, "2")]
[InlineData ("AB12", 1, 5, "2\n1\nB\nA")]
[InlineData ("AB\n12", 2, 5, "2B\n1A")]
[InlineData ("ABC 123 456", 2, 7, "C6\nB5\nA4\n \n 3\n 2\n 1")]
[InlineData ("こんにちは", 1, 1, "")]
[InlineData ("こんにちは", 2, 1, "は")]
[InlineData ("こんにちは", 2, 5, "は\nち\nに\nん\nこ")]
[InlineData ("こんにちは", 2, 10, "は\nち\nに\nん\nこ")]
[InlineData ("こんにちは\nAB\n12", 4, 10, "2Bは\n1Aち\n に\n ん\n こ")]
public void Draw_Vertical_BottomTop_RightLeft (string text, int width, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.BottomTop_RightLeft
};
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
[InlineData ("A", 5, 5, "A")]
[InlineData (
"AB12",
5,
5,
@"
A
B
1
2")]
[InlineData (
"AB\n12",
5,
5,
@"
A1
B2")]
[InlineData ("", 5, 1, "")]
[InlineData (
"Hello Worlds",
1,
12,
@"
H
e
l
l
o
W
o
r
l
d
s")]
[InlineData ("Hello Worlds", 12, 1, @"HelloWorlds")]
public void Draw_Vertical_TopBottom_LeftRight (string text, int width, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.TopBottom_LeftRight
};
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, 20, 20), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
// The expectedY param is to probe that the expectedText param start at that Y coordinate
[InlineData ("A", 0, "", 0)]
[InlineData ("A", 1, "A", 0)]
[InlineData ("A", 2, "A", 0)]
[InlineData ("A", 3, "A", 1)]
[InlineData ("AB", 1, "A", 0)]
[InlineData ("AB", 2, "A\nB", 0)]
[InlineData ("ABC", 2, "A\nB", 0)]
[InlineData ("ABC", 3, "A\nB\nC", 0)]
[InlineData ("ABC", 4, "A\nB\nC", 0)]
[InlineData ("ABC", 5, "A\nB\nC", 1)]
[InlineData ("ABC", 6, "A\nB\nC", 1)]
[InlineData ("ABC", 9, "A\nB\nC", 3)]
[InlineData ("ABCD", 2, "B\nC", 0)]
[InlineData ("こんにちは", 0, "", 0)]
[InlineData ("こんにちは", 1, "に", 0)]
[InlineData ("こんにちは", 2, "ん\nに", 0)]
[InlineData ("こんにちは", 3, "ん\nに\nち", 0)]
[InlineData ("こんにちは", 4, "こ\nん\nに\nち", 0)]
[InlineData ("こんにちは", 5, "こ\nん\nに\nち\nは", 0)]
[InlineData ("こんにちは", 6, "こ\nん\nに\nち\nは", 0)]
[InlineData ("ABCD\nこんにちは", 7, "Aこ\nBん\nCに\nDち\n は", 1)]
[InlineData ("こんにちは\nABCD", 7, "こA\nんB\nにC\nちD\nは ", 1)]
public void Draw_Vertical_TopBottom_LeftRight_Middle (string text, int height, string expectedText, int expectedY)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.TopBottom_LeftRight,
VerticalAlignment = Alignment.Center
};
int width = text.ToRunes ().Max (r => r.GetColumns ());
if (text.Contains ("\n"))
{
width++;
}
tf.ConstrainToWidth = width;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, 5, height), Attribute.Default, Attribute.Default, driver: driver);
Rectangle rect = DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
Assert.Equal (expectedY, rect.Y);
}
[Theory]
[InlineData ("A", 5, "A")]
[InlineData (
"AB12",
5,
@"
A
B
1
2")]
[InlineData (
"AB\n12",
5,
@"
A1
B2")]
[InlineData ("", 1, "")]
[InlineData (
"AB1 2",
2,
@"
A12
B ")]
[InlineData (
"こんにちは",
1,
@"
こん")]
[InlineData (
"こんにちは",
2,
@"
こに
んち")]
[InlineData (
"こんにちは",
5,
@"
は")]
public void Draw_Vertical_TopBottom_LeftRight_Top (string text, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Direction = TextDirection.TopBottom_LeftRight
};
tf.ConstrainToWidth = 5;
tf.ConstrainToHeight = height;
tf.Draw (new (0, 0, 5, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Fact]
public void FillRemaining_True_False ()
{
IDriver driver = CreateFakeDriver ();
driver.SetScreenSize (22, 5);
Attribute [] attrs =
{
Attribute.Default, new (ColorName16.Green, ColorName16.BrightMagenta),
new (ColorName16.Blue, ColorName16.Cyan)
};
var tf = new TextFormatter { ConstrainToSize = new (14, 3), Text = "Test\nTest long\nTest long long\n", MultiLine = true };
tf.Draw (
new (1, 1, 19, 3),
attrs [1],
attrs [2],
driver: driver);
Assert.False (tf.FillRemaining);
DriverAssert.AssertDriverContentsWithFrameAre (
@"
Test
Test long
Test long long",
output,
driver);
DriverAssert.AssertDriverAttributesAre (
@"
000000000000000000000
011110000000000000000
011111111100000000000
011111111111111000000
000000000000000000000",
output,
driver,
attrs);
tf.FillRemaining = true;
tf.Draw (
new (1, 1, 19, 3),
attrs [1],
attrs [2],
driver: driver);
DriverAssert.AssertDriverAttributesAre (
@"
000000000000000000000
011111111111111111110
011111111111111111110
011111111111111111110
000000000000000000000",
output,
driver,
attrs);
}
[Theory]
[InlineData ("Hello World", 15, 1, "Hello World")]
[InlineData (
"Well Done\nNice Work",
15,
2,
@"
Well Done
Nice Work")]
[InlineData ("你好 世界", 15, 1, "你好 世界")]
[InlineData (
"做 得好\n幹 得好",
15,
2,
@"
做 得好
幹 得好")]
public void Justify_Horizontal (string text, int width, int height, string expectedText)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = text,
Alignment = Alignment.Fill,
ConstrainToSize = new Size (width, height),
MultiLine = true
};
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Fact]
public void UICatalog_AboutBox_Text ()
{
IDriver? driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Text = UICatalogTop.GetAboutBoxMessage (),
Alignment = Alignment.Center,
VerticalAlignment = Alignment.Start,
WordWrap = false,
MultiLine = true,
HotKeySpecifier = (Rune)0xFFFF
};
Size tfSize = tf.FormatAndGetSize ();
Assert.Equal (new (59, 13), tfSize);
driver!.SetScreenSize (tfSize.Width, tfSize.Height);
driver.FillRect (driver.Screen, (Rune)'*');
tf.Draw (driver.Screen, Attribute.Default, Attribute.Default, driver: driver);
var expectedText = """
UI Catalog: A comprehensive sample library and test app for
***********************************************************
_______ _ _ _____ _ *
|__ __| (_) | | / ____| (_)*
| | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ *
| |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | |*
| | __/ | | | | | | | | | | | (_| | || |__| | |_| | |*
|_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_|*
***********************************************************
**********************v2 - Pre-Alpha***********************
***********************************************************
**********https://github.com/gui-cs/Terminal.Gui***********
***********************************************************
""";
DriverAssert.AssertDriverContentsAre (expectedText.ReplaceLineEndings (), output, driver);
}
#region FormatAndGetSizeTests
// TODO: Add multi-line examples
// TODO: Add other TextDirection examples
[Theory]
[InlineData ("界1234", 10, 10, TextDirection.LeftRight_TopBottom, 6, 1, @"界1234")]
[InlineData ("01234", 10, 10, TextDirection.LeftRight_TopBottom, 5, 1, @"01234")]
[InlineData (
"界1234",
10,
10,
TextDirection.TopBottom_LeftRight,
2,
5,
"""
1
2
3
4
""")]
[InlineData (
"01234",
10,
10,
TextDirection.TopBottom_LeftRight,
1,
5,
"""
0
1
2
3
4
""")]
[InlineData (
"界1234",
3,
3,
TextDirection.LeftRight_TopBottom,
3,
2,
"""
界1
234
""")]
[InlineData (
"01234",
3,
3,
TextDirection.LeftRight_TopBottom,
3,
2,
"""
012
34
""")]
[InlineData (
"界1234",
3,
3,
TextDirection.TopBottom_LeftRight,
3,
3,
"""
界3
1 4
2
""")]
[InlineData (
"01234",
3,
3,
TextDirection.TopBottom_LeftRight,
2,
3,
"""
03
14
2
""")]
[InlineData ("01234", 2, 1, TextDirection.LeftRight_TopBottom, 2, 1, @"01")]
public void FormatAndGetSize_Returns_Correct_Size (
string text,
int width,
int height,
TextDirection direction,
int expectedWidth,
int expectedHeight,
string expectedDraw
)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Direction = direction,
ConstrainToWidth = width,
ConstrainToHeight = height,
Text = text
};
Assert.True (tf.WordWrap);
Size size = tf.FormatAndGetSize ();
Assert.Equal (new (expectedWidth, expectedHeight), size);
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedDraw, output, driver);
}
[Theory]
[InlineData ("界1234", 10, 10, TextDirection.LeftRight_TopBottom, 6, 1, @"界1234")]
[InlineData ("01234", 10, 10, TextDirection.LeftRight_TopBottom, 5, 1, @"01234")]
[InlineData (
"界1234",
10,
10,
TextDirection.TopBottom_LeftRight,
2,
5,
"""
1
2
3
4
""")]
[InlineData (
"01234",
10,
10,
TextDirection.TopBottom_LeftRight,
1,
5,
"""
0
1
2
3
4
""")]
[InlineData ("界1234", 3, 3, TextDirection.LeftRight_TopBottom, 3, 1, @"界1")]
[InlineData ("01234", 3, 3, TextDirection.LeftRight_TopBottom, 3, 1, @"012")]
[InlineData (
"界1234",
3,
3,
TextDirection.TopBottom_LeftRight,
2,
3,
"""
1
2
""")]
[InlineData (
"01234",
3,
3,
TextDirection.TopBottom_LeftRight,
1,
3,
"""
0
1
2
""")]
public void FormatAndGetSize_WordWrap_False_Returns_Correct_Size (
string text,
int width,
int height,
TextDirection direction,
int expectedWidth,
int expectedHeight,
string expectedDraw
)
{
IDriver driver = CreateFakeDriver ();
TextFormatter tf = new ()
{
Direction = direction,
ConstrainToSize = new (width, height),
Text = text,
WordWrap = false
};
Assert.False (tf.WordWrap);
Size size = tf.FormatAndGetSize ();
Assert.Equal (new (expectedWidth, expectedHeight), size);
tf.Draw (new (0, 0, width, height), Attribute.Default, Attribute.Default, driver: driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedDraw, output, driver);
}
#endregion
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,18 +2,10 @@
using UnitTests;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests_Parallelizable.TextTests;
public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
public class TextFormatterTests (ITestOutputHelper output) : FakeDriverBase
{
private readonly ITestOutputHelper _output;
public TextFormatterTests (ITestOutputHelper output)
{
_output = output;
}
[Theory]
[InlineData ("")]
[InlineData (null)]
@@ -720,7 +712,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
foreach (Alignment alignment in Enum.GetValues (typeof (Alignment)))
{
TextFormatter tf = new ()
{ Text = text, ConstrainToSize = new (1, maxWidth), WordWrap = wrap, VerticalAlignment = alignment, Direction = TextDirection.TopBottom_LeftRight };
{ Text = text, ConstrainToSize = new (1, maxWidth), WordWrap = wrap, VerticalAlignment = alignment, Direction = TextDirection.TopBottom_LeftRight };
List<string> list = TextFormatter.Format (
text,
@@ -2913,7 +2905,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
[InlineData ("This has lf in the end\n", "This has lf in the end")]
public void StripCRLF_RemovesCrLf (string input, string expected)
{
string actual = TextFormatter.StripCRLF(input, keepNewLine: false);
string actual = TextFormatter.StripCRLF (input, keepNewLine: false);
Assert.Equal (expected, actual);
}
@@ -2937,7 +2929,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
[InlineData ("This has lf in the end\n", "This has lf in the end\n")]
public void StripCRLF_KeepNewLine_RemovesCarriageReturnFromCrLf (string input, string expected)
{
string actual = TextFormatter.StripCRLF(input, keepNewLine: true);
string actual = TextFormatter.StripCRLF (input, keepNewLine: true);
Assert.Equal (expected, actual);
}
@@ -2961,7 +2953,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
[InlineData ("This has lf in the end\n", "This has lf in the end ")]
public void ReplaceCRLFWithSpace_ReplacesCrLfWithSpace (string input, string expected)
{
string actual = TextFormatter.ReplaceCRLFWithSpace(input);
string actual = TextFormatter.ReplaceCRLFWithSpace (input);
Assert.Equal (expected, actual);
}
@@ -2986,7 +2978,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
public void Draw_Horizontal_Centered (string text, int width, string expectedText)
{
var driver = CreateFakeDriver (width > 0 ? width : 1, 1);
TextFormatter tf = new ()
{
Text = text,
@@ -2997,7 +2989,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, _output, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
@@ -3015,7 +3007,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
public void Draw_Horizontal_Justified (string text, int width, string expectedText)
{
var driver = CreateFakeDriver (width > 0 ? width : 1, 1);
TextFormatter tf = new ()
{
Text = text,
@@ -3026,7 +3018,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, _output, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
@@ -3041,7 +3033,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
public void Draw_Horizontal_Left (string text, int width, string expectedText)
{
var driver = CreateFakeDriver (width > 0 ? width : 1, 1);
TextFormatter tf = new ()
{
Text = text,
@@ -3052,7 +3044,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, _output, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
@@ -3067,7 +3059,7 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
public void Draw_Horizontal_Right (string text, int width, string expectedText)
{
var driver = CreateFakeDriver (width > 0 ? width : 1, 1);
TextFormatter tf = new ()
{
Text = text,
@@ -3078,6 +3070,162 @@ public class TextFormatterTests : UnitTests.Parallelizable.ParallelizableBase
tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default, default, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, _output, driver);
DriverAssert.AssertDriverContentsWithFrameAre (expectedText, output, driver);
}
[Theory]
[InlineData (14, 1, TextDirection.LeftRight_TopBottom, "Les Misęrables")]
[InlineData (1, 14, TextDirection.TopBottom_LeftRight, "L\ne\ns\n \nM\ni\ns\nę\nr\na\nb\nl\ne\ns")]
[InlineData (
4,
4,
TextDirection.TopBottom_LeftRight,
@"
LMre
eias
ssb
ęl "
)]
public void Draw_With_Combining_Runes (int width, int height, TextDirection textDirection, string expected)
{
var driver = CreateFakeDriver ();
var text = "Les Mise\u0328\u0301rables";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.Text = text;
Assert.True (tf.WordWrap);
tf.ConstrainToSize = new (width, height);
tf.Draw (
new (0, 0, width, height),
new (ColorName16.White, ColorName16.Black),
new (ColorName16.Blue, ColorName16.Black),
default (Rectangle),
driver
);
DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
[Theory]
[InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")]
[InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")]
[InlineData (13, 1, TextDirection.LeftRight_TopBottom, 0, "This is a Tab")]
[InlineData (1, 13, TextDirection.TopBottom_LeftRight, 0, "T\nh\ni\ns\n \ni\ns\n \na\n \nT\na\nb")]
public void TabWith_PreserveTrailingSpaces_True (
int width,
int height,
TextDirection textDirection,
int tabWidth,
string expected
)
{
var driver = CreateFakeDriver ();
var text = "This is a \tTab";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.TabWidth = tabWidth;
tf.PreserveTrailingSpaces = true;
tf.Text = text;
tf.ConstrainToWidth = 20;
tf.ConstrainToHeight = 20;
Assert.True (tf.WordWrap);
tf.Draw (
new (0, 0, width, height),
new (ColorName16.White, ColorName16.Black),
new (ColorName16.Blue, ColorName16.Black),
default (Rectangle),
driver
);
DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
[Theory]
[InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")]
[InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")]
[InlineData (13, 1, TextDirection.LeftRight_TopBottom, 0, "This is a Tab")]
[InlineData (1, 13, TextDirection.TopBottom_LeftRight, 0, "T\nh\ni\ns\n \ni\ns\n \na\n \nT\na\nb")]
public void TabWith_WordWrap_True (
int width,
int height,
TextDirection textDirection,
int tabWidth,
string expected
)
{
var driver = CreateFakeDriver ();
var text = "This is a \tTab";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.TabWidth = tabWidth;
tf.WordWrap = true;
tf.Text = text;
tf.ConstrainToWidth = 20;
tf.ConstrainToHeight = 20;
Assert.False (tf.PreserveTrailingSpaces);
tf.Draw (
new (0, 0, width, height),
new (ColorName16.White, ColorName16.Black),
new (ColorName16.Blue, ColorName16.Black),
default (Rectangle),
driver
);
DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
[Theory]
[InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")]
[InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")]
[InlineData (13, 1, TextDirection.LeftRight_TopBottom, 0, "This is a Tab")]
[InlineData (1, 13, TextDirection.TopBottom_LeftRight, 0, "T\nh\ni\ns\n \ni\ns\n \na\n \nT\na\nb")]
public void TabWith_PreserveTrailingSpaces_False (
int width,
int height,
TextDirection textDirection,
int tabWidth,
string expected
)
{
var driver = CreateFakeDriver ();
var text = "This is a \tTab";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.TabWidth = tabWidth;
tf.Text = text;
tf.ConstrainToWidth = 20;
tf.ConstrainToHeight = 20;
Assert.True (tf.WordWrap);
Assert.False (tf.PreserveTrailingSpaces);
tf.Draw (
new (0, 0, width, height),
new (ColorName16.White, ColorName16.Black),
new (ColorName16.Blue, ColorName16.Black),
default (Rectangle),
driver
);
DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
}

View File

@@ -29,7 +29,6 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\UnitTests\TestsAllViews.cs" Link="TestsAllViews.cs" />
<Compile Include="..\UnitTests\DriverAssert.cs" Link="DriverAssert.cs" />
<Compile Include="..\UnitTests\OutputAssert.cs" Link="OutputAssert.cs" />
</ItemGroup>
@@ -53,9 +52,11 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Examples\UICatalog\UICatalog.csproj" />
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj">
</ProjectReference>
<ProjectReference Include="..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
<ProjectReference Include="..\UnitTests\UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
@@ -68,23 +69,4 @@
<Using Include="Terminal.Gui" />
<Using Include="Xunit" />
</ItemGroup>
<PropertyGroup Label="FineCodeCoverage">
<Enabled>
False
</Enabled>
<Exclude>
[UICatalog]*
</Exclude>
<Include></Include>
<ExcludeByFile>
<!--**/Migrations/*
**/Hacks/*.cs-->
</ExcludeByFile>
<ExcludeByAttribute>
<!--MyCustomExcludeFromCodeCoverage-->
</ExcludeByAttribute>
<IncludeTestAssembly>
False
</IncludeTestAssembly>
</PropertyGroup>
</Project>

View File

@@ -13,7 +13,6 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
/// events: KeyDown and KeyDownNotHandled. Note that KeyUp is independent.
/// </summary>
[Theory]
//[SetupFakeDriver] // Required for spinner view that wants to register timeouts
[MemberData (nameof (AllViewTypes))]
public void AllViews_NewKeyDownEvent_All_EventsFire (Type viewType)
{
@@ -56,7 +55,6 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
/// KeyUp
/// </summary>
[Theory]
//[SetupFakeDriver] // Required for spinner view that wants to register timeouts
[MemberData (nameof (AllViewTypes))]
public void AllViews_NewKeyUpEvent_All_EventsFire (Type viewType)
{

View File

@@ -1,11 +1,11 @@
#nullable enable
using UnitTests.Parallelizable;
using UnitTests;
using Xunit;
namespace UnitTests_Parallelizable.ViewTests;
[Trait ("Category", "View.Scheme")]
public class SchemeTests : ParallelizableBase
public class SchemeTests : FakeDriverBase
{
[Fact]

View File

@@ -0,0 +1,57 @@
#nullable enable
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ViewsTests;
public class AllViewsDrawTests (ITestOutputHelper output) : TestsAllViews
{
[Theory]
[MemberData (nameof (AllViewTypes))]
public void AllViews_Draw_Does_Not_Layout (Type viewType)
{
IDriver driver = CreateFakeDriver ();
View? view = CreateInstanceIfNotGeneric (viewType);
if (view is null)
{
output.WriteLine ($"Ignoring {viewType} - It's a Generic");
return;
}
output.WriteLine ($"Testing {viewType}");
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
var drawCompleteCount = 0;
view.DrawComplete += (s, e) => drawCompleteCount++;
var layoutStartedCount = 0;
view.SubViewLayout += (s, e) => layoutStartedCount++;
var layoutCompleteCount = 0;
view.SubViewsLaidOut += (s, e) => layoutCompleteCount++;
view.SetNeedsLayout ();
view.Layout ();
Assert.Equal (0, drawCompleteCount);
Assert.Equal (1, layoutStartedCount);
Assert.Equal (1, layoutCompleteCount);
if (view.Visible)
{
view.SetNeedsDraw ();
view.Draw ();
Assert.Equal (1, drawCompleteCount);
Assert.Equal (1, layoutStartedCount);
Assert.Equal (1, layoutCompleteCount);
}
}
}

View File

@@ -1,26 +1,108 @@
using System.Reflection;
using Microsoft.VisualStudio.TestPlatform.Utilities;
#nullable enable
using System.Reflection;
using UnitTests;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ViewsTests;
[Collection ("Global Test Setup")]
public class AllViewsTests (ITestOutputHelper output) : TestsAllViews
{
[Theory]
[MemberData (nameof (AllViewTypes))]
public void AllViews_Layout_Does_Not_Draw (Type viewType)
{
IDriver driver = CreateFakeDriver ();
View? view = CreateInstanceIfNotGeneric (viewType);
if (view is null)
{
output.WriteLine ($"Ignoring {viewType} - It's a Generic");
return;
}
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
var drawContentCount = 0;
view.DrawingContent += (s, e) => drawContentCount++;
var layoutStartedCount = 0;
view.SubViewLayout += (s, e) => layoutStartedCount++;
var layoutCompleteCount = 0;
view.SubViewsLaidOut += (s, e) => layoutCompleteCount++;
view.SetNeedsLayout ();
view.SetNeedsDraw ();
view.Layout ();
Assert.Equal (0, drawContentCount);
Assert.Equal (1, layoutStartedCount);
Assert.Equal (1, layoutCompleteCount);
}
[Theory]
[MemberData (nameof (AllViewTypes))]
public void AllViews_Center_Properly (Type viewType)
{
IDriver driver = CreateFakeDriver ();
View? view = CreateInstanceIfNotGeneric (viewType);
if (view is null)
{
output.WriteLine ($"Ignoring {viewType} - It's a Generic");
return;
}
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
view.X = Pos.Center ();
view.Y = Pos.Center ();
// Ensure the view has positive dimensions
view.Width = 10;
view.Height = 10;
var frame = new View { X = 0, Y = 0, Width = 50, Height = 50 };
frame.Add (view);
frame.LayoutSubViews ();
frame.Dispose ();
// What's the natural width/height?
int expectedX = (frame.Frame.Width - view.Frame.Width) / 2;
int expectedY = (frame.Frame.Height - view.Frame.Height) / 2;
Assert.True (
view.Frame.Left == expectedX,
$"{view} did not center horizontally. Expected: {expectedX}. Actual: {view.Frame.Left}"
);
Assert.True (
view.Frame.Top == expectedY,
$"{view} did not center vertically. Expected: {expectedY}. Actual: {view.Frame.Top}"
);
}
[Theory]
[MemberData (nameof (AllViewTypes))]
public void AllViews_Tests_All_Constructors (Type viewType)
{
Assert.True (TestAllConstructorsOfType (viewType));
return;
bool TestAllConstructorsOfType (Type type)
{
foreach (ConstructorInfo ctor in type.GetConstructors ())
{
View view = CreateViewFromType (type, ctor);
View? view = CreateViewFromType (type, ctor);
if (view != null)
{

View File

@@ -1,10 +1,12 @@
using UnitTests;
namespace UnitTests_Parallelizable.ViewsTests;
/// <summary>
/// Pure unit tests for <see cref="Button"/> that don't require Application static dependencies.
/// These tests can run in parallel without interference.
/// </summary>
public class ButtonTests : UnitTests.Parallelizable.ParallelizableBase
public class ButtonTests : FakeDriverBase
{
[Fact]
public void Text_Mirrors_Title ()

View File

@@ -1,10 +1,12 @@
using UnitTests;
namespace UnitTests_Parallelizable.ViewsTests;
/// <summary>
/// Pure unit tests for <see cref="ColorPicker"/> that don't require Application.Driver or View context.
/// These tests can run in parallel without interference.
/// </summary>
public class ColorPickerTests : UnitTests.Parallelizable.ParallelizableBase
public class ColorPickerTests : FakeDriverBase
{
[Fact]
public void ColorPicker_ChangedEvent_Fires ()

View File

@@ -1,4 +1,5 @@
using System.Globalization;
using UnitTests;
namespace UnitTests_Parallelizable.ViewsTests;
@@ -6,7 +7,7 @@ namespace UnitTests_Parallelizable.ViewsTests;
/// Pure unit tests for <see cref="DatePicker"/> that don't require Application.Driver or View context.
/// These tests can run in parallel without interference.
/// </summary>
public class DatePickerTests : UnitTests.Parallelizable.ParallelizableBase
public class DatePickerTests : FakeDriverBase
{
[Fact]
public void DatePicker_ChangingCultureChangesFormat ()

View File

@@ -1,10 +1,12 @@
using UnitTests;
namespace UnitTests_Parallelizable.ViewsTests;
/// <summary>
/// Pure unit tests for <see cref="Label"/> that don't require Application.Driver or Application context.
/// These tests can run in parallel without interference.
/// </summary>
public class LabelTests : UnitTests.Parallelizable.ParallelizableBase
public class LabelTests : FakeDriverBase
{
[Fact]
public void Text_Mirrors_Title ()

View File

@@ -1,8 +1,9 @@
using System.Text;
using UnitTests;
namespace UnitTests_Parallelizable.ViewsTests;
public class SliderOptionTests : UnitTests.Parallelizable.ParallelizableBase
public class SliderOptionTests : FakeDriverBase
{
[Fact]
public void OnChanged_Should_Raise_ChangedEvent ()
@@ -94,7 +95,7 @@ public class SliderOptionTests : UnitTests.Parallelizable.ParallelizableBase
}
}
public class SliderEventArgsTests : UnitTests.Parallelizable.ParallelizableBase
public class SliderEventArgsTests : FakeDriverBase
{
[Fact]
public void Constructor_Sets_Cancel_Default_To_False ()
@@ -138,7 +139,7 @@ public class SliderEventArgsTests : UnitTests.Parallelizable.ParallelizableBase
}
}
public class SliderTests : UnitTests.Parallelizable.ParallelizableBase
public class SliderTests : FakeDriverBase
{
[Fact]
public void Constructor_Default ()

View File

@@ -1,11 +1,10 @@
using System.Text;
using UnitTests;
using UnitTests.Parallelizable;
using Xunit.Abstractions;
namespace UnitTests_Parallelizable.ViewsTests;
public class TextFieldTests (ITestOutputHelper output) : ParallelizableBase
public class TextFieldTests (ITestOutputHelper output) : FakeDriverBase
{
[Fact]
public void Cancel_TextChanging_ThenBackspace ()
@@ -561,7 +560,7 @@ public class TextFieldTests (ITestOutputHelper output) : ParallelizableBase
[Fact]
public void Accented_Letter_With_Three_Combining_Unicode_Chars ()
{
IConsoleDriver driver = CreateFakeDriver ();
IDriver driver = CreateFakeDriver ();
var tf = new TextField { Width = 3, Text = "ắ" };
tf.Driver = driver;
@@ -612,7 +611,7 @@ public class TextFieldTests (ITestOutputHelper output) : ParallelizableBase
[Fact]
public void Adjust_First ()
{
IConsoleDriver driver = CreateFakeDriver ();
IDriver driver = CreateFakeDriver ();
var tf = new TextField { Width = Dim.Fill (), Text = "This is a test." };
tf.Driver = driver;

View File

@@ -1,8 +1,9 @@
using System.Text.RegularExpressions;
using UnitTests;
namespace UnitTests_Parallelizable.ViewsTests;
public class TextValidateField_NET_Provider_Tests : UnitTests.Parallelizable.ParallelizableBase
public class TextValidateField_NET_Provider_Tests : FakeDriverBase
{
[Fact]
public void Backspace_Key_Deletes_Previous_Character ()
@@ -425,7 +426,7 @@ public class TextValidateField_NET_Provider_Tests : UnitTests.Parallelizable.Par
}
}
public class TextValidateField_Regex_Provider_Tests : UnitTests.Parallelizable.ParallelizableBase
public class TextValidateField_Regex_Provider_Tests : FakeDriverBase
{
[Fact]
public void End_Key_End_Of_Input ()

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>opencover</Format>
<Exclude>[UnitTests]*,[UICatalog]*,[coverlet.*.tests?]*,[*]Coverlet.Core*</Exclude> <!-- [Assembly-Filter]Type-Filter -->
<Include></Include> <!-- [Assembly-Filter]Type-Filter -->
<ExcludeByAttribute></ExcludeByAttribute>
<ExcludeByFile></ExcludeByFile> <!-- Globbing filter -->
<IncludeDirectory></IncludeDirectory>
<SingleHit>false</SingleHit>
<UseSourceLink>true</UseSourceLink>
<IncludeTestAssembly>true</IncludeTestAssembly>
<SkipAutoProps>true</SkipAutoProps>
<DeterministicReport>false</DeterministicReport>
<ResultsDirectory>TestResults/</ResultsDirectory>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<!-- Existing Coverlet/Code Coverage Config -->
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat Code Coverage">
<Configuration>
<Format>cobertura</Format>
<ExcludeByFile>**/obj/**/*.*</ExcludeByFile>
<Exclude>[*]*.g.cs</Exclude>
<Exclude>[*]*AssemblyInfo.cs</Exclude>
<!-- ONLY include Terminal.Gui assembly -->
<Include>[Terminal.Gui]*</Include>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
<!-- New: xUnit Config for Stop-on-Fail -->
<xUnit>
<ParallelizeAssembly>true</ParallelizeAssembly>
<ParallelizeTestCollections>true</ParallelizeTestCollections>
<!-- Enable collection parallelism -->
<MaxParallelThreads>unlimited</MaxParallelThreads>
<!-- Or 'unlimited' / '2x' for CPU multiplier -->
<StopOnFail>true</StopOnFail>
<!-- Still stop on first failure -->
</xUnit>
<!-- Optional: Global Run Config (e.g., results dir) -->
<RunConfiguration>
<ResultsDirectory>../TestResults</ResultsDirectory>
</RunConfiguration>
</RunSettings>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<!-- New: xUnit Config for Stop-on-Fail -->
<xUnit>
<ParallelizeAssembly>true</ParallelizeAssembly>
<ParallelizeTestCollections>true</ParallelizeTestCollections>
<!-- Enable collection parallelism -->
<MaxParallelThreads>unlimited</MaxParallelThreads>
<!-- Or 'unlimited' / '2x' for CPU multiplier -->
<StopOnFail>true</StopOnFail>
<!-- Still stop on first failure -->
</xUnit>
</RunSettings>