diff --git a/Tests/UnitTests/Text/TextFormatterTests.cs b/Tests/UnitTests/Text/TextFormatterTests.cs index 1f1e7819c..7af163eab 100644 --- a/Tests/UnitTests/Text/TextFormatterTests.cs +++ b/Tests/UnitTests/Text/TextFormatterTests.cs @@ -3828,7 +3828,7 @@ ssb [SetupFakeDriver] public void FillRemaining_True_False () { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (22, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (22, 5); Attribute [] attrs = { @@ -4050,7 +4050,7 @@ Nice Work")] Size tfSize = tf.FormatAndGetSize (); Assert.Equal (new (59, 13), tfSize); - ((IFakeConsoleDriver)Application.Driver).SetBufferSize (tfSize.Width, tfSize.Height); + ((FakeDriver)Application.Driver).SetBufferSize (tfSize.Width, tfSize.Height); Application.Driver.FillRect (Application.Screen, (Rune)'*'); tf.Draw (Application.Screen, Attribute.Default, Attribute.Default); diff --git a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs index b8ff317e6..a5eb3f2de 100644 --- a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs +++ b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs @@ -30,7 +30,7 @@ public class ShadowStyleTests (ITestOutputHelper output) [SetupFakeDriver] public void ShadowView_Colors (ShadowStyle style, string expectedAttrs) { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 5); Color fg = Color.Red; Color bg = Color.Green; @@ -100,7 +100,7 @@ public class ShadowStyleTests (ITestOutputHelper output) [SetupFakeDriver] public void Visual_Test (ShadowStyle style, string expected) { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (5, 5); + ((FakeDriver)Application.Driver!).SetBufferSize (5, 5); var superView = new Toplevel { diff --git a/Tests/UnitTests/View/TextTests.cs b/Tests/UnitTests/View/TextTests.cs index f8062c744..7744c666f 100644 --- a/Tests/UnitTests/View/TextTests.cs +++ b/Tests/UnitTests/View/TextTests.cs @@ -998,7 +998,7 @@ w "; [SetupFakeDriver] public void Narrow_Wide_Runes () { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (32, 32); + ((FakeDriver)Application.Driver!).SetBufferSize (32, 32); var top = new View { Width = 32, Height = 32 }; var text = $"First line{Environment.NewLine}Second line"; diff --git a/Tests/UnitTests/Views/LabelTests.cs b/Tests/UnitTests/Views/LabelTests.cs index aacc98efd..c6b22981a 100644 --- a/Tests/UnitTests/Views/LabelTests.cs +++ b/Tests/UnitTests/Views/LabelTests.cs @@ -892,7 +892,7 @@ e [SetupFakeDriver] public void Label_Height_Zero_Stays_Zero () { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (10, 4); + ((FakeDriver)Application.Driver!).SetBufferSize (10, 4); var text = "Label"; var label = new Label diff --git a/Tests/UnitTests/Views/TableViewTests.cs b/Tests/UnitTests/Views/TableViewTests.cs index 27d77328c..98594707c 100644 --- a/Tests/UnitTests/Views/TableViewTests.cs +++ b/Tests/UnitTests/Views/TableViewTests.cs @@ -2206,7 +2206,7 @@ public class TableViewTests (ITestOutputHelper output) [SetupFakeDriver] public void TestEnumerableDataSource_BasicTypes () { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); var tv = new TableView (); tv.SchemeName = "TopLevel"; tv.Viewport = new (0, 0, 50, 6); diff --git a/Tests/UnitTests/Views/ToplevelTests.cs b/Tests/UnitTests/Views/ToplevelTests.cs index 3160fb314..e47e1cdd9 100644 --- a/Tests/UnitTests/Views/ToplevelTests.cs +++ b/Tests/UnitTests/Views/ToplevelTests.cs @@ -507,10 +507,10 @@ public class ToplevelTests top.BeginInit (); top.EndInit (); - Exception exception = Record.Exception (() => ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (0, 10)); + Exception exception = Record.Exception (() => ((FakeDriver)Application.Driver!).SetBufferSize (0, 10)); Assert.Null (exception); - exception = Record.Exception (() => ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (10, 0)); + exception = Record.Exception (() => ((FakeDriver)Application.Driver!).SetBufferSize (10, 0)); Assert.Null (exception); } diff --git a/Tests/UnitTests/Views/TreeTableSourceTests.cs b/Tests/UnitTests/Views/TreeTableSourceTests.cs index 6d3c972be..7dd09e84a 100644 --- a/Tests/UnitTests/Views/TreeTableSourceTests.cs +++ b/Tests/UnitTests/Views/TreeTableSourceTests.cs @@ -30,7 +30,7 @@ public class TreeTableSourceTests : IDisposable [SetupFakeDriver] public void TestTreeTableSource_BasicExpanding_WithKeyboard () { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); TableView tv = GetTreeTable (out _); tv.Style.GetOrCreateColumnStyle (1).MinAcceptableWidth = 1; @@ -91,7 +91,7 @@ public class TreeTableSourceTests : IDisposable [SetupFakeDriver] public void TestTreeTableSource_BasicExpanding_WithMouse () { - ((IFakeConsoleDriver)Application.Driver!).SetBufferSize (100, 100); + ((FakeDriver)Application.Driver!).SetBufferSize (100, 100); TableView tv = GetTreeTable (out _); diff --git a/Tests/UnitTestsParallelizable/DriverAssert.cs b/Tests/UnitTestsParallelizable/DriverAssert.cs new file mode 100644 index 000000000..7f504cd5f --- /dev/null +++ b/Tests/UnitTestsParallelizable/DriverAssert.cs @@ -0,0 +1,391 @@ +using System.Text; +using System.Text.RegularExpressions; +using Xunit.Abstractions; + +namespace UnitTests_Parallelizable; + +/// +/// Provides xUnit-style assertions for contents. +/// +internal partial class DriverAssert +{ + private const char SpaceChar = ' '; + private static readonly Rune SpaceRune = (Rune)SpaceChar; +#pragma warning disable xUnit1013 // Public method should be marked as test + /// + /// Verifies are found at the locations specified by + /// . is a bitmap of indexes into + /// (e.g. "00110" means the attribute at expectedAttributes[1] is expected + /// at the 3rd and 4th columns of the 1st row of driver.Contents). + /// + /// + /// Numbers between 0 and 9 for each row/col of the console. Must be valid indexes into + /// . + /// + /// + /// The IConsoleDriver to use. If null will be used. + /// + public static void AssertDriverAttributesAre ( + string expectedLook, + ITestOutputHelper output, + IConsoleDriver driver = null, + params Attribute [] expectedAttributes + ) + { +#pragma warning restore xUnit1013 // Public method should be marked as test + + if (expectedAttributes.Length > 10) + { + throw new ArgumentException ("This method only works for UIs that use at most 10 colors"); + } + + expectedLook = expectedLook.Trim (); + driver ??= Application.Driver; + + Cell [,] contents = driver!.Contents; + + var line = 0; + + foreach (string lineString in expectedLook.Split ('\n').Select (l => l.Trim ())) + { + for (var c = 0; c < lineString.Length; c++) + { + Attribute? val = contents! [line, c].Attribute; + + List match = expectedAttributes.Where (e => e == val).ToList (); + + switch (match.Count) + { + case 0: + output.WriteLine ( + $"{Application.ToString (driver)}\n" + + $"Expected Attribute {val} at Contents[{line},{c}] {contents [line, c]} was not found.\n" + + $" Expected: {string.Join (",", expectedAttributes.Select (attr => attr))}\n" + + $" But Was: " + ); + Assert.Empty (match); + + return; + case > 1: + throw new ArgumentException ( + $"Bad value for expectedColors, {match.Count} Attributes had the same Value" + ); + } + + char colorUsed = Array.IndexOf (expectedAttributes, match [0]).ToString () [0]; + char userExpected = lineString [c]; + + if (colorUsed != userExpected) + { + output.WriteLine ($"{Application.ToString (driver)}"); + output.WriteLine ($"Unexpected Attribute at Contents[{line},{c}] = {contents [line, c]}."); + output.WriteLine ($" Expected: {userExpected} ({expectedAttributes [int.Parse (userExpected.ToString ())]})"); + output.WriteLine ($" But Was: {colorUsed} ({val})"); + + // Print `contents` as the expected and actual attribute indexes in a grid where each cell is of the form "e:a" (e = expected, a = actual) + // e.g: + // 0:1 0:0 1:1 + // 0:0 1:1 0:0 + // 0:0 1:1 0:0 + + //// Use StringBuilder since output only has .WriteLine + //var sb = new StringBuilder (); + //// for each line in `contents` + //for (var r = 0; r < driver.Rows; r++) + //{ + // // for each column in `contents` + // for (var cc = 0; cc < driver.Cols; cc++) + // { + // // get the attribute at the current location + // Attribute? val2 = contents [r, cc].Attribute; + // // if the attribute is not null + // if (val2.HasValue) + // { + // // get the index of the attribute in `expectedAttributes` + // int index = Array.IndexOf (expectedAttributes, val2.Value); + // // if the index is -1, it means the attribute was not found in `expectedAttributes` + + // // get the index of the actual attribute in `expectedAttributes` + + + // if (index == -1) + // { + // sb.Append ("x:x "); + // } + // else + // { + // sb.Append ($"{index}:{val2.Value} "); + // } + // } + // else + // { + // sb.Append ("x:x "); + // } + // } + // sb.AppendLine (); + //} + + //output.WriteLine ($"Contents:\n{sb}"); + + Assert.Equal (userExpected, colorUsed); + + return; + } + } + + line++; + } + } + +#pragma warning disable xUnit1013 // Public method should be marked as test + /// Asserts that the driver contents match the expected contents, optionally ignoring any trailing whitespace. + /// + /// + /// The IConsoleDriver to use. If null will be used. + /// + public static void AssertDriverContentsAre ( + string expectedLook, + ITestOutputHelper output, + IConsoleDriver driver = null, + bool ignoreLeadingWhitespace = false + ) + { +#pragma warning restore xUnit1013 // Public method should be marked as test + var actualLook = Application.ToString (driver ?? Application.Driver); + + if (string.Equals (expectedLook, actualLook)) + { + return; + } + + // get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string) + expectedLook = TrailingWhiteSpaceRegEx ().Replace (expectedLook, "").Trim (); + actualLook = TrailingWhiteSpaceRegEx ().Replace (actualLook, "").Trim (); + + if (ignoreLeadingWhitespace) + { + expectedLook = LeadingWhitespaceRegEx ().Replace (expectedLook, "").Trim (); + actualLook = LeadingWhitespaceRegEx ().Replace (actualLook, "").Trim (); + } + + // standardize line endings for the comparison + expectedLook = expectedLook.Replace ("\r\n", "\n"); + actualLook = actualLook.Replace ("\r\n", "\n"); + + // If test is about to fail show user what things looked like + if (!string.Equals (expectedLook, actualLook)) + { + output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook); + output?.WriteLine (" But Was:" + Environment.NewLine + actualLook); + } + + Assert.Equal (expectedLook, actualLook); + } + + /// + /// Asserts that the driver contents are equal to the provided string. + /// + /// + /// + /// The IConsoleDriver to use. If null will be used. + /// + public static Rectangle AssertDriverContentsWithFrameAre ( + string expectedLook, + ITestOutputHelper output, + IConsoleDriver driver = null + ) + { + List> lines = new (); + var sb = new StringBuilder (); + driver ??= Application.Driver; + int x = -1; + int y = -1; + int w = -1; + int h = -1; + + Cell [,] contents = driver.Contents; + + for (var rowIndex = 0; rowIndex < driver.Rows; rowIndex++) + { + List runes = []; + + for (var colIndex = 0; colIndex < driver.Cols; colIndex++) + { + Rune runeAtCurrentLocation = contents [rowIndex, colIndex].Rune; + + if (runeAtCurrentLocation != SpaceRune) + { + if (x == -1) + { + x = colIndex; + y = rowIndex; + + for (var i = 0; i < colIndex; i++) + { + runes.InsertRange (i, [SpaceRune]); + } + } + + if (runeAtCurrentLocation.GetColumns () > 1) + { + colIndex++; + } + + if (colIndex + 1 > w) + { + w = colIndex + 1; + } + + h = rowIndex - y + 1; + } + + if (x > -1) + { + runes.Add (runeAtCurrentLocation); + } + + // See Issue #2616 + //foreach (var combMark in contents [r, c].CombiningMarks) { + // runes.Add (combMark); + //} + } + + if (runes.Count > 0) + { + lines.Add (runes); + } + } + + // Remove unnecessary empty lines + if (lines.Count > 0) + { + for (int r = lines.Count - 1; r > h - 1; r--) + { + lines.RemoveAt (r); + } + } + + // Remove trailing whitespace on each line + foreach (List row in lines) + { + for (int c = row.Count - 1; c >= 0; c--) + { + Rune rune = row [c]; + + if (rune != (Rune)' ' || row.Sum (x => x.GetColumns ()) == w) + { + break; + } + + row.RemoveAt (c); + } + } + + // Convert Rune list to string + for (var r = 0; r < lines.Count; r++) + { + var line = StringExtensions.ToString (lines [r]); + + if (r == lines.Count - 1) + { + sb.Append (line); + } + else + { + sb.AppendLine (line); + } + } + + var actualLook = sb.ToString (); + + if (string.Equals (expectedLook, actualLook)) + { + return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0); + } + + // standardize line endings for the comparison + expectedLook = expectedLook.ReplaceLineEndings (); + actualLook = actualLook.ReplaceLineEndings (); + + // Remove the first and the last line ending from the expectedLook + if (expectedLook.StartsWith (Environment.NewLine)) + { + expectedLook = expectedLook [Environment.NewLine.Length..]; + } + + if (expectedLook.EndsWith (Environment.NewLine)) + { + expectedLook = expectedLook [..^Environment.NewLine.Length]; + } + + // If test is about to fail show user what things looked like + if (!string.Equals (expectedLook, actualLook)) + { + output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook); + output?.WriteLine (" But Was:" + Environment.NewLine + actualLook); + } + + Assert.Equal (expectedLook, actualLook); + + return new (x > -1 ? x : 0, y > -1 ? y : 0, w > -1 ? w : 0, h > -1 ? h : 0); + } + + + /// + /// Verifies the console used all the when rendering. If one or more of the + /// expected colors are not used then the failure will output both the colors that were found to be used and which of + /// your expectations was not met. + /// + /// if null uses + /// + internal static void AssertDriverUsedColors (IConsoleDriver driver = null, params Attribute [] expectedColors) + { + driver ??= Application.Driver; + Cell [,] contents = driver.Contents; + + List toFind = expectedColors.ToList (); + + // Contents 3rd column is an Attribute + HashSet colorsUsed = new (); + + for (var r = 0; r < driver.Rows; r++) + { + for (var c = 0; c < driver.Cols; c++) + { + Attribute? val = contents [r, c].Attribute; + + if (val.HasValue) + { + colorsUsed.Add (val.Value); + + Attribute match = toFind.FirstOrDefault (e => e == val); + + // need to check twice because Attribute is a struct and therefore cannot be null + if (toFind.Any (e => e == val)) + { + toFind.Remove (match); + } + } + } + } + + if (!toFind.Any ()) + { + return; + } + + var sb = new StringBuilder (); + sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => a.ToString ()))); + sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (a => a.ToString ()))); + + throw new (sb.ToString ()); + } + + + [GeneratedRegex ("^\\s+", RegexOptions.Multiline)] + private static partial Regex LeadingWhitespaceRegEx (); + + + [GeneratedRegex ("\\s+$", RegexOptions.Multiline)] + private static partial Regex TrailingWhiteSpaceRegEx (); +} diff --git a/Tests/UnitTestsParallelizable/Drivers/FakeDriverRenderingTests.cs b/Tests/UnitTestsParallelizable/Drivers/FakeDriverRenderingTests.cs index 5542b0b82..d850f8e0f 100644 --- a/Tests/UnitTestsParallelizable/Drivers/FakeDriverRenderingTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/FakeDriverRenderingTests.cs @@ -4,69 +4,59 @@ using Xunit.Abstractions; namespace UnitTests_Parallelizable.Drivers; /// -/// Tests for FakeDriver functionality including rendering and basic driver operations. +/// Tests for FakeDriver functionality including basic driver operations. /// These tests prove that FakeDriver can be used independently for testing Terminal.Gui applications. /// public class FakeDriverRenderingTests (ITestOutputHelper output) { private readonly ITestOutputHelper _output = output; - #region View Rendering Tests + #region Basic Driver Tests [Fact] - public void FakeDriver_Can_Render_Simple_Label () + public void FakeDriver_Can_Write_To_Contents_Buffer () { // Arrange var driver = new FakeDriver (); driver.Init (); - var label = new Label { Text = "Hello World", X = 0, Y = 0 }; - label.Driver = driver; - label.BeginInit (); - label.EndInit (); + // Act - Write directly to driver + driver.Move (0, 0); + driver.AddStr ("Hello World"); - // Act - label.SetNeedsDraw (); - label.Draw (); - - // Assert + // Assert - Verify text was written to driver contents Assert.NotNull (driver.Contents); - Assert.Equal (80, driver.Cols); - Assert.Equal (25, driver.Rows); + + // Check that "Hello World" is in the first row + string firstRow = ""; + for (int col = 0; col < Math.Min (11, driver.Cols); col++) + { + firstRow += (char)driver.Contents [0, col].Rune.Value; + } + Assert.Equal ("Hello World", firstRow); driver.End (); - label.Dispose (); } [Fact] - public void FakeDriver_Can_Render_View_With_Border () + public void FakeDriver_Can_Set_Attributes () { // Arrange var driver = new FakeDriver (); driver.Init (); - - var window = new Window - { - Title = "Test Window", - X = 0, - Y = 0, - Width = 40, - Height = 10, - BorderStyle = LineStyle.Single - }; - window.Driver = driver; - window.BeginInit (); - window.EndInit (); + var attr = new Attribute (Color.Red, Color.Blue); // Act - window.SetNeedsDraw (); - window.Draw (); + driver.Move (5, 5); + driver.SetAttribute (attr); + driver.AddRune ('X'); - // Assert - Check that contents buffer was written to + // Assert - Verify attribute was set Assert.NotNull (driver.Contents); - + Assert.Equal ('X', (char)driver.Contents [5, 5].Rune.Value); + Assert.Equal (attr, driver.Contents [5, 5].Attribute); + driver.End (); - window.Dispose (); } [Fact] @@ -100,5 +90,44 @@ public class FakeDriverRenderingTests (ITestOutputHelper output) driver.End (); } + [Fact] + public void FakeDriver_Can_Fill_Rectangle () + { + // Arrange + var driver = new FakeDriver (); + driver.Init (); + + // Act + driver.FillRect (new Rectangle (0, 0, 5, 3), '*'); + + // Assert - Verify rectangle was filled + for (int row = 0; row < 3; row++) + { + for (int col = 0; col < 5; col++) + { + Assert.Equal ('*', (char)driver.Contents [row, col].Rune.Value); + } + } + + driver.End (); + } + + [Fact] + public void FakeDriver_Tracks_Cursor_Position () + { + // Arrange + var driver = new FakeDriver (); + driver.Init (); + + // Act + driver.Move (10, 5); + + // Assert + Assert.Equal (10, driver.Col); + Assert.Equal (5, driver.Row); + + driver.End (); + } + #endregion }