From f5ec13144175ad76f891d1f9748497d0ec8689ee Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 15 Mar 2025 01:06:41 +0000 Subject: [PATCH 01/12] Fixes #3986. ContextMenus now broken in v2 net and win drivers --- Terminal.Gui/Views/Menu/MenuBar.cs | 37 +++++++++++----- Tests/UnitTests/Views/ContextMenuTests.cs | 53 ++++++++++++----------- Tests/UnitTests/Views/MenuBarTests.cs | 30 ++++++++----- 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 416acb653b..9ae2709f06 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -441,7 +441,15 @@ out OpenCurrentMenu._currentChild return; } - Application.GrabMouse (this); + if (_isContextMenuLoading) + { + Application.GrabMouse (_openMenu); + _isContextMenuLoading = false; + } + else + { + Application.GrabMouse (this); + } } /// @@ -493,6 +501,11 @@ internal void Activate (int idx, int sIdx = -1, MenuBarItem? subMenu = null!) internal void CleanUp () { + if (_isCleaning) + { + return; + } + _isCleaning = true; if (_openMenu is { }) @@ -1448,9 +1461,9 @@ protected override bool OnMouseEvent (MouseEventArgs me) Activate (i); } } - else if (me.Flags == MouseFlags.Button1Pressed - || me.Flags == MouseFlags.Button1DoubleClicked - || me.Flags == MouseFlags.Button1TripleClicked) + else if (me.Flags.HasFlag (MouseFlags.Button1Pressed) + || me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) + || me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) { if (IsMenuOpen && !Menus [i].IsTopLevel) { @@ -1534,16 +1547,17 @@ internal bool HandleGrabView (MouseEventArgs me, View current) } } - if (me.View != current) + if (Application.MouseGrabView != me.View) { - View v = current; - Application.UngrabMouse (); + View v = me.View; + Application.GrabMouse (v); - if (((Menu)me.View).Host.SuperView is { } && ((Menu)me.View).Host.SuperView!.InternalSubViews.Contains(me.View)) - { - v = me.View; - } + return true; + } + if (me.View != current) + { + View v = me.View; Application.GrabMouse (v); MouseEventArgs nme; @@ -1590,7 +1604,6 @@ internal bool HandleGrabView (MouseEventArgs me, View current) else { _handled = false; - _isContextMenuLoading = false; return false; } diff --git a/Tests/UnitTests/Views/ContextMenuTests.cs b/Tests/UnitTests/Views/ContextMenuTests.cs index 17a60e5725..e68f5e729a 100644 --- a/Tests/UnitTests/Views/ContextMenuTests.cs +++ b/Tests/UnitTests/Views/ContextMenuTests.cs @@ -76,7 +76,7 @@ public void ContextMenu_Is_Closed_If_Another_MenuBar_Is_Open_Or_Vice_Versa () ] ); - var menu = new MenuBar + var menuBar = new MenuBar { Menus = [ @@ -86,24 +86,26 @@ public void ContextMenu_Is_Closed_If_Another_MenuBar_Is_Open_Or_Vice_Versa () }; var top = new Toplevel (); - top.Add (menu); + top.Add (menuBar); Application.Begin (top); Assert.Null (Application.MouseGrabView); cm.Show (menuItems); Assert.True (ContextMenu.IsShow); - Assert.Equal (cm.MenuBar, Application.MouseGrabView); - Assert.False (menu.IsMenuOpen); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (ContextMenu.IsShow); + Menu menu = (Menu)top.SubViews.First (v => v is Menu); Assert.Equal (menu, Application.MouseGrabView); - Assert.True (menu.IsMenuOpen); + Assert.False (menuBar.IsMenuOpen); + Assert.True (menuBar.NewKeyDownEvent (menuBar.Key)); + Assert.False (ContextMenu.IsShow); + Assert.Equal (menuBar, Application.MouseGrabView); + Assert.True (menuBar.IsMenuOpen); cm.Show (menuItems); Assert.True (ContextMenu.IsShow); - Assert.Equal (cm.MenuBar, Application.MouseGrabView); - Assert.False (menu.IsMenuOpen); + menu = (Menu)top.SubViews.First (v => v is Menu); + Assert.Equal (menu, Application.MouseGrabView); + Assert.False (menuBar.IsMenuOpen); #if SUPPORT_ALT_TO_ACTIVATE_MENU Assert.True (Application.Top.ProcessKeyUp (new (Key.AltMask))); Assert.False (ContextMenu.IsShow); @@ -113,16 +115,17 @@ public void ContextMenu_Is_Closed_If_Another_MenuBar_Is_Open_Or_Vice_Versa () cm.Show (menuItems); Assert.True (ContextMenu.IsShow); - Assert.Equal (cm.MenuBar, Application.MouseGrabView); - Assert.False (menu.IsMenuOpen); - Assert.False (menu.NewMouseEvent (new MouseEventArgs { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu })); + menu = (Menu)top.SubViews.First (v => v is Menu); + Assert.Equal (menu, Application.MouseGrabView); + Assert.False (menuBar.IsMenuOpen); + Assert.False (menuBar.NewMouseEvent (new MouseEventArgs { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menuBar })); Assert.True (ContextMenu.IsShow); - Assert.Equal (cm.MenuBar, Application.MouseGrabView); - Assert.False (menu.IsMenuOpen); - Assert.True (menu.NewMouseEvent (new MouseEventArgs { Position = new (1, 0), Flags = MouseFlags.Button1Clicked, View = menu })); - Assert.False (ContextMenu.IsShow); Assert.Equal (menu, Application.MouseGrabView); - Assert.True (menu.IsMenuOpen); + Assert.False (menuBar.IsMenuOpen); + Assert.True (menuBar.NewMouseEvent (new MouseEventArgs { Position = new (1, 0), Flags = MouseFlags.Button1Clicked, View = menuBar })); + Assert.False (ContextMenu.IsShow); + Assert.Equal (menuBar, Application.MouseGrabView); + Assert.True (menuBar.IsMenuOpen); top.Dispose (); } @@ -1423,7 +1426,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Assert.Equal (6, win.SubViews.Count); Assert.True (tf2.ContextMenu.MenuBar.IsMenuOpen); Assert.True (win.Focused is Menu); - Assert.True (Application.MouseGrabView is MenuBar); + Assert.True (Application.MouseGrabView is Menu); Assert.Equal (tf2, Application._cachedViewsUnderMouse.LastOrDefault ()); // Click on tf1 to focus it, which cause context menu being closed @@ -2026,15 +2029,15 @@ public void Mouse_Pressed_Released_Clicked (int button) // Right Button case 3: Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Pressed }); - Assert.False (menuBar.IsMenuOpen); + Assert.True (menuBar.IsMenuOpen); Application.MainLoop.RunIteration (); Assert.False (actionRaised); Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Released }); - Assert.False (menuBar.IsMenuOpen); + Assert.True (menuBar.IsMenuOpen); Application.MainLoop.RunIteration (); Assert.False (actionRaised); Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Clicked }); - Assert.False (menuBar.IsMenuOpen); + Assert.True (menuBar.IsMenuOpen); Application.MainLoop.RunIteration (); Assert.False (actionRaised); @@ -2042,7 +2045,7 @@ public void Mouse_Pressed_Released_Clicked (int button) } // ContextMenu - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 2), Flags = cm.MouseFlags }); + Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = cm.MouseFlags }); Assert.False (menuBar.IsMenuOpen); Assert.True (cm.MenuBar!.IsMenuOpen); @@ -2050,15 +2053,15 @@ public void Mouse_Pressed_Released_Clicked (int button) { // Left Button case 1: - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Pressed }); + Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 6), Flags = MouseFlags.Button1Pressed }); Assert.True (cm.MenuBar!.IsMenuOpen); Application.MainLoop.RunIteration (); Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Released }); + Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 6), Flags = MouseFlags.Button1Released }); Assert.True (cm.MenuBar!.IsMenuOpen); Application.MainLoop.RunIteration (); Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button1Clicked }); + Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 6), Flags = MouseFlags.Button1Clicked }); Assert.False (cm.MenuBar!.IsMenuOpen); Application.MainLoop.RunIteration (); Assert.True (actionRaised); diff --git a/Tests/UnitTests/Views/MenuBarTests.cs b/Tests/UnitTests/Views/MenuBarTests.cs index e5428e7b09..b65fdf9968 100644 --- a/Tests/UnitTests/Views/MenuBarTests.cs +++ b/Tests/UnitTests/Views/MenuBarTests.cs @@ -2518,7 +2518,7 @@ public void MouseEvent_Test () MenuItem miCurrent = null; Menu mCurrent = null; - var menu = new MenuBar + var menuBar = new MenuBar { Menus = [ @@ -2533,22 +2533,22 @@ public void MouseEvent_Test () ] }; - menu.MenuOpened += (s, e) => + menuBar.MenuOpened += (s, e) => { miCurrent = e.MenuItem; - mCurrent = menu.OpenCurrentMenu; + mCurrent = menuBar.OpenCurrentMenu; }; var top = new Toplevel (); - top.Add (menu); + top.Add (menuBar); Application.Begin (top); // Click on Edit Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menu } + menuBar.NewMouseEvent ( + new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menuBar } ) ); - Assert.True (menu.IsMenuOpen); + Assert.True (menuBar.IsMenuOpen); Assert.Equal ("_Edit", miCurrent.Parent.Title); Assert.Equal ("_Copy", miCurrent.Title); @@ -2558,7 +2558,7 @@ public void MouseEvent_Test () new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } ) ); - Assert.True (menu.IsMenuOpen); + Assert.True (menuBar.IsMenuOpen); Assert.Equal ("_Edit", miCurrent.Parent.Title); Assert.Equal ("_Paste", miCurrent.Title); @@ -2568,8 +2568,18 @@ public void MouseEvent_Test () new () { ScreenPosition = new (10, i), Flags = MouseFlags.ReportMousePosition } ); - Assert.True (menu.IsMenuOpen); - Assert.Equal (menu, Application.MouseGrabView); + Assert.True (menuBar.IsMenuOpen); + Menu menu = (Menu)top.SubViews.First (v => v is Menu); + + if (i is < 0 or > 0) + { + Assert.Equal (menu, Application.MouseGrabView); + } + else + { + Assert.Equal (menuBar, Application.MouseGrabView); + } + Assert.Equal ("_Edit", miCurrent.Parent.Title); if (i == 4) From e483a0373506c69096245e9481b197d6327f1e3d Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 13:28:29 +0000 Subject: [PATCH 02/12] Investigate fluent assertions for testing v2 --- Terminal.Gui/Terminal.Gui.csproj | 1 + Terminal.sln | 6 + TerminalGuiFluentAssertions/Class1.cs | 155 ++++++++++++++++++ .../TerminalGuiFluentAssertions.csproj | 13 ++ .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 2 +- .../FluentTests/BasicFluentAssertionTests.cs | 35 ++++ Tests/UnitTests/UnitTests.csproj | 1 + 7 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 TerminalGuiFluentAssertions/Class1.cs create mode 100644 TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj create mode 100644 Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index e19e3521f9..39b32ede9a 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -86,6 +86,7 @@ + diff --git a/Terminal.sln b/Terminal.sln index 0399a634d4..c233b37ad5 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -62,6 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentAssertions", "TerminalGuiFluentAssertions\TerminalGuiFluentAssertions.csproj", "{7C610F03-9E38-409F-9B21-A02D5569E16A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +118,10 @@ Global {DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.Build.0 = Release|Any CPU + {7C610F03-9E38-409F-9B21-A02D5569E16A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C610F03-9E38-409F-9B21-A02D5569E16A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C610F03-9E38-409F-9B21-A02D5569E16A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C610F03-9E38-409F-9B21-A02D5569E16A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs new file mode 100644 index 0000000000..596be8d47c --- /dev/null +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -0,0 +1,155 @@ +using System.Collections.Concurrent; +using System.Drawing; +using Terminal.Gui; + +namespace TerminalGuiFluentAssertions; + +class FakeInput : IConsoleInput +{ + /// + public void Dispose () { } + + /// + public void Initialize (ConcurrentQueue inputBuffer) { } + + /// + public void Run (CancellationToken token) + { + // Simulate an infinite loop that checks for cancellation + token.WaitHandle.WaitOne (); // Blocks until the token is cancelled + } +} + +class FakeNetInput : FakeInput, INetInput +{ + +} + +class FakeWindowsInput : FakeInput, IWindowsInput +{ + +} + +class FakeOutput : IConsoleOutput +{ + public Size Size { get; set; } + + /// + public void Dispose () + { + + } + + /// + public void Write (ReadOnlySpan text) + { + + } + + /// + public void Write (IOutputBuffer buffer) + { + + } + + /// + public Size GetWindowSize () + { + return Size; + } + + /// + public void SetCursorVisibility (CursorVisibility visibility) + { + + } + + /// + public void SetCursorPosition (int col, int row) + { + + } + +} +/// +/// Entry point to fluent assertions. +/// +public static class With +{ + /// + /// Entrypoint to fluent assertions + /// + /// + /// + /// + public static GuiTestContext A (int width, int height) where T : Toplevel, new () + { + return new GuiTestContext (width,height); + } +} +public class GuiTestContext where T : Toplevel, new() +{ + private readonly CancellationTokenSource _cts; + private readonly Task _runTask; + + internal GuiTestContext (int width, int height) + { + IApplication origApp = ApplicationImpl.Instance; + + var netInput = new FakeNetInput (); + var winInput = new FakeWindowsInput (); + var output = new FakeOutput (); + + output.Size = new (width, height); + + var v2 = new ApplicationV2( + () => netInput, + ()=>output, + () => winInput, + () => output); + + // Create a cancellation token + _cts = new (); + + // Start the application in a background thread + _runTask = Task.Run (() => + { + try + { + ApplicationImpl.ChangeInstance (v2); + + v2.Init (); + + Application.Run (); // This will block, but it's on a background thread now + + Application.Shutdown (); + } + catch (OperationCanceledException) + { + + } + finally + { + ApplicationImpl.ChangeInstance (origApp); + } + }, _cts.Token); + + Application.Shutdown (); + } + + /// + /// Stops the application and waits for the background thread to exit. + /// + public void Stop () + { + _cts.Cancel (); + Application.Invoke (()=>Application.RequestStop()); + _runTask.Wait (); // Ensure the background thread exits + } + + // Cleanup to avoid state bleed between tests + public void Dispose () + { + } +} + diff --git a/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj b/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj new file mode 100644 index 0000000000..629e31cbed --- /dev/null +++ b/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 1a87b6b593..c1006cf510 100644 --- a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -6,7 +6,7 @@ namespace UnitTests.ConsoleDrivers.V2; public class ApplicationV2Tests { - + private ApplicationV2 NewApplicationV2 () { var netInput = new Mock (); diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs new file mode 100644 index 0000000000..1a3473c6c9 --- /dev/null +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TerminalGuiFluentAssertions; + +namespace UnitTests.FluentTests; +public class BasicFluentAssertionTests +{ + [Fact] + public void GuiTestContext_StartsAndStopsWithoutError () + { + var context = With.A (40, 10); + + // No actual assertions are needed — if no exceptions are thrown, it's working + context.Stop (); + } + + [Fact] + public void Test () + { + var myView = new TextField () { Width = 10 }; + + + + /* + using var ctx = With.A (20, 10) + .Add (myView, 3, 2) + .Focus (myView) + .Type ("Hello"); + + Assert.Equal ("Hello", myView.Text);*/ + } +} diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index fa84707494..27dd519faa 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -45,6 +45,7 @@ + From c7d26e620901cb3e4cdee7f24b650386aef4be69 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 14:50:58 +0000 Subject: [PATCH 03/12] WIP: Add fluent assertions and prototype the kind of things that should be possible --- Directory.Packages.props | 93 ++++++++-------- TerminalGuiFluentAssertions/Class1.cs | 103 ++++++++++++++---- .../TerminalGuiFluentAssertions.csproj | 4 + .../FluentTests/BasicFluentAssertionTests.cs | 29 +++-- 4 files changed, 147 insertions(+), 82 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 18afbe64a4..fbed063776 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,52 +1,45 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 596be8d47c..0ab4296f5f 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -1,10 +1,12 @@ using System.Collections.Concurrent; using System.Drawing; +using FluentAssertions; +using FluentAssertions.Numeric; using Terminal.Gui; namespace TerminalGuiFluentAssertions; -class FakeInput : IConsoleInput +class FakeInput(CancellationToken hardStopToken) : IConsoleInput { /// public void Dispose () { } @@ -15,17 +17,17 @@ public void Initialize (ConcurrentQueue inputBuffer) { } /// public void Run (CancellationToken token) { - // Simulate an infinite loop that checks for cancellation - token.WaitHandle.WaitOne (); // Blocks until the token is cancelled + // Blocks until either the token or the hardStopToken is cancelled. + WaitHandle.WaitAny (new [] { token.WaitHandle, hardStopToken.WaitHandle }); } } -class FakeNetInput : FakeInput, INetInput +class FakeNetInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), INetInput { } -class FakeWindowsInput : FakeInput, IWindowsInput +class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), IWindowsInput { } @@ -87,29 +89,29 @@ public static class With return new GuiTestContext (width,height); } } -public class GuiTestContext where T : Toplevel, new() +public class GuiTestContext : IDisposable where T : Toplevel, new() { - private readonly CancellationTokenSource _cts; + private readonly CancellationTokenSource _cts = new (); + private readonly CancellationTokenSource _hardStop = new (); private readonly Task _runTask; + private Exception _ex; + private readonly FakeOutput _output = new (); internal GuiTestContext (int width, int height) { IApplication origApp = ApplicationImpl.Instance; - var netInput = new FakeNetInput (); - var winInput = new FakeWindowsInput (); - var output = new FakeOutput (); + var netInput = new FakeNetInput (_cts.Token); + var winInput = new FakeWindowsInput (_cts.Token); - output.Size = new (width, height); + _output.Size = new (width, height); var v2 = new ApplicationV2( () => netInput, - ()=>output, + ()=>_output, () => winInput, - () => output); + () => _output); - // Create a cancellation token - _cts = new (); // Start the application in a background thread _runTask = Task.Run (() => @@ -125,31 +127,90 @@ internal GuiTestContext (int width, int height) Application.Shutdown (); } catch (OperationCanceledException) + { } + catch (Exception ex) { - + _ex = ex; } finally { ApplicationImpl.ChangeInstance (origApp); } }, _cts.Token); - - Application.Shutdown (); } /// /// Stops the application and waits for the background thread to exit. /// - public void Stop () + public GuiTestContext Stop () { + if (_runTask.IsCompleted) + { + return this; + } + + Application.Invoke (()=> Application.RequestStop ()); + + // Wait for the application to stop, but give it a 1-second timeout + if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000))) + { + _cts.Cancel (); + // Timeout occurred, force the task to stop + _hardStop.Cancel (); + throw new TimeoutException ("Application failed to stop within the allotted time."); + } _cts.Cancel (); - Application.Invoke (()=>Application.RequestStop()); - _runTask.Wait (); // Ensure the background thread exits + + if (_ex != null) + { + throw _ex; // Propagate any exception that happened in the background task + } + + return this; } // Cleanup to avoid state bleed between tests public void Dispose () { + Stop (); + _hardStop.Cancel(); + } + + /// + /// Adds the given to the current top level view + /// and performs layout. + /// + /// + /// + public GuiTestContext Add (View v) + { + Application.Invoke ( + () => + { + var top = Application.Top ?? throw new Exception("Top was null so could not add view"); + top.Add (v); + top.Layout (); + }); + + return this; + } + + public GuiTestContext ResizeConsole (int width, int height) + { + _output.Size = new Size (width,height); + + return WaitIteration (); + } + public GuiTestContext WaitIteration () + { + Application.Invoke (() => { }); + + return this; + } + + public GuiTestContext Assert (AndConstraint be) + { + return this; } } diff --git a/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj b/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj index 629e31cbed..5654a72359 100644 --- a/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj +++ b/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index 1a3473c6c9..0f35ede618 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using FluentAssertions; using TerminalGuiFluentAssertions; namespace UnitTests.FluentTests; @@ -11,25 +12,31 @@ public class BasicFluentAssertionTests [Fact] public void GuiTestContext_StartsAndStopsWithoutError () { - var context = With.A (40, 10); + using var context = With.A (40, 10); // No actual assertions are needed — if no exceptions are thrown, it's working context.Stop (); } [Fact] - public void Test () + public void GuiTestContext_ForgotToStop () { - var myView = new TextField () { Width = 10 }; - - + using var context = With.A (40, 10); + } - /* - using var ctx = With.A (20, 10) - .Add (myView, 3, 2) - .Focus (myView) - .Type ("Hello"); + [Fact] + public void TestWindowsResize () + { + var lbl = new Label () + { + Width = Dim.Fill () + }; + using var c = With.A (40, 10) + .Add (lbl ) + .Assert (lbl.Frame.Width.Should().Be(40)) + .ResizeConsole (20,20) + .Assert (lbl.Frame.Width.Should ().Be (20)) + .Stop (); - Assert.Equal ("Hello", myView.Text);*/ } } From 8c105ae1aadee34b4a3414ce12b523bb3371180f Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 15:39:33 +0000 Subject: [PATCH 04/12] Make tests pass --- TerminalGuiFluentAssertions/Class1.cs | 33 ++++++++++++++----- .../FluentTests/BasicFluentAssertionTests.cs | 4 +-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 0ab4296f5f..0075e62217 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -184,13 +184,13 @@ public void Dispose () /// public GuiTestContext Add (View v) { - Application.Invoke ( - () => - { - var top = Application.Top ?? throw new Exception("Top was null so could not add view"); - top.Add (v); - top.Layout (); - }); + WaitIteration ( + () => + { + var top = Application.Top ?? throw new Exception("Top was null so could not add view"); + top.Add (v); + top.Layout (); + }); return this; } @@ -201,10 +201,25 @@ public GuiTestContext ResizeConsole (int width, int height) return WaitIteration (); } - public GuiTestContext WaitIteration () + public GuiTestContext WaitIteration (Action? a = null) { - Application.Invoke (() => { }); + a ??= () => { }; + var ctsLocal = new CancellationTokenSource (); + + + Application.Invoke (()=> + { + a(); + ctsLocal.Cancel (); + }); + // Blocks until either the token or the hardStopToken is cancelled. + WaitHandle.WaitAny (new [] + { + _cts.Token.WaitHandle, + _hardStop.Token.WaitHandle, + ctsLocal.Token.WaitHandle + }); return this; } diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index 0f35ede618..b52d071885 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -33,9 +33,9 @@ public void TestWindowsResize () }; using var c = With.A (40, 10) .Add (lbl ) - .Assert (lbl.Frame.Width.Should().Be(40)) + .Assert (lbl.Frame.Width.Should().Be(38)) // Window has 2 border .ResizeConsole (20,20) - .Assert (lbl.Frame.Width.Should ().Be (20)) + .Assert (lbl.Frame.Width.Should ().Be (18)) .Stop (); } From 8bf35761cb246999bcf298e06b6609d9cb32f12c Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 17:15:06 +0000 Subject: [PATCH 05/12] WIP: try to get context menu to work properly in automation tests --- TerminalGuiFluentAssertions/Class1.cs | 138 +++++++++++++++++- .../FluentTests/BasicFluentAssertionTests.cs | 24 +++ 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 0075e62217..127e98f6cd 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -3,6 +3,8 @@ using FluentAssertions; using FluentAssertions.Numeric; using Terminal.Gui; +using Terminal.Gui.ConsoleDrivers; +using static Unix.Terminal.Curses; namespace TerminalGuiFluentAssertions; @@ -12,7 +14,9 @@ class FakeInput(CancellationToken hardStopToken) : IConsoleInput public void Dispose () { } /// - public void Initialize (ConcurrentQueue inputBuffer) { } + public void Initialize (ConcurrentQueue inputBuffer) { InputBuffer = inputBuffer;} + + public ConcurrentQueue InputBuffer { get; set; } /// public void Run (CancellationToken token) @@ -96,13 +100,15 @@ public static class With private readonly Task _runTask; private Exception _ex; private readonly FakeOutput _output = new (); + private readonly FakeWindowsInput winInput; + private View _lastView; internal GuiTestContext (int width, int height) { IApplication origApp = ApplicationImpl.Instance; var netInput = new FakeNetInput (_cts.Token); - var winInput = new FakeWindowsInput (_cts.Token); + winInput = new FakeWindowsInput (_cts.Token); _output.Size = new (width, height); @@ -120,7 +126,7 @@ internal GuiTestContext (int width, int height) { ApplicationImpl.ChangeInstance (v2); - v2.Init (); + v2.Init (null,"v2win"); Application.Run (); // This will block, but it's on a background thread now @@ -137,6 +143,8 @@ internal GuiTestContext (int width, int height) ApplicationImpl.ChangeInstance (origApp); } }, _cts.Token); + + WaitIteration (); } /// @@ -190,6 +198,7 @@ public GuiTestContext Add (View v) var top = Application.Top ?? throw new Exception("Top was null so could not add view"); top.Add (v); top.Layout (); + _lastView = v; }); return this; @@ -227,5 +236,128 @@ public GuiTestContext Assert (AndConstraint be) { return this; } + + public GuiTestContext RightClick (int screenX, int screenY) + { + return Click (WindowsConsole.ButtonState.Button3Pressed,screenX, screenY); + } + + public GuiTestContext LeftClick (int screenX, int screenY) + { + return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); + } + + private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY) + { + winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new WindowsConsole.MouseEventRecord () + { + ButtonState = btn, + MousePosition = new WindowsConsole.Coord ((short)screenX, (short)screenY) + } + }); + + winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new WindowsConsole.MouseEventRecord () + { + ButtonState = WindowsConsole.ButtonState.NoButtonPressed, + MousePosition = new WindowsConsole.Coord ((short)screenX, (short)screenY) + } + }); + + WaitIteration (); + + return this; + } + + public GuiTestContext Down () + { + winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord + { + bKeyDown = true, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord + { + bKeyDown = false, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + + WaitIteration (); + + return this; + } + public GuiTestContext Enter () + { + winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord + { + bKeyDown = true, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord + { + bKeyDown = false, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + WaitIteration (); + + return this; + } + public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems) + { + LastView.MouseEvent += (s, e) => + { + if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) + { + ctx.Show (menuItems); + } + }; + + return this; + } + + public View LastView => _lastView ?? Application.Top ?? throw new Exception ("Could not determine which view to add to"); + + } diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index b52d071885..2c896c47a0 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -37,6 +37,30 @@ public void TestWindowsResize () .ResizeConsole (20,20) .Assert (lbl.Frame.Width.Should ().Be (18)) .Stop (); + } + + [Fact] + public void ContextMenu_CrashesOnRight () + { + var clicked = false; + + var ctx = new ContextMenu (); + var menuItems = new MenuBarItem ( + [ + new ("_New File", string.Empty, () => { clicked = true; }) + ] + ); + + using var c = With.A (40, 10) + .WithContextMenu(ctx,menuItems) + // Click in main area inside border + .RightClick(1,1) + .LeftClick (2, 2) + /*.Assert (Application.Top.Focused.Should ().BeAssignableTo(typeof(MenuBarItem))) + .Down() + .Enter()*/ + .Stop (); + Assert.True (clicked); } } From c14bb90cbd7c78dab7d92e2710a0b91cfe4a2eca Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 19:10:00 +0000 Subject: [PATCH 06/12] Add screenshot method --- TerminalGuiFluentAssertions/Class1.cs | 13 ++++++++++- .../FluentTests/BasicFluentAssertionTests.cs | 23 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 127e98f6cd..3d0e65cbfa 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -38,6 +38,7 @@ class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput @@ -55,9 +56,10 @@ public void Write (ReadOnlySpan text) /// public void Write (IOutputBuffer buffer) { - + LastBuffer = buffer; } + /// public Size GetWindowSize () { @@ -210,6 +212,15 @@ public GuiTestContext ResizeConsole (int width, int height) return WaitIteration (); } + public GuiTestContext ScreenShot (string title, TextWriter writer) + { + writer.WriteLine(title); + var text = Application.ToString (); + + writer.WriteLine(text); + + return WaitIteration (); + } public GuiTestContext WaitIteration (Action? a = null) { a ??= () => { }; diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index 2c896c47a0..9015be5f08 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -5,10 +5,30 @@ using System.Threading.Tasks; using FluentAssertions; using TerminalGuiFluentAssertions; +using Xunit.Abstractions; namespace UnitTests.FluentTests; public class BasicFluentAssertionTests { + private readonly TextWriter _out; + public class TestOutputWriter : TextWriter + { + private readonly ITestOutputHelper _output; + + public TestOutputWriter (ITestOutputHelper output) + { + _output = output; + } + + public override void WriteLine (string? value) + { + _output.WriteLine (value ?? string.Empty); + } + + public override Encoding Encoding => Encoding.UTF8; + } + + public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter(outputHelper); } [Fact] public void GuiTestContext_StartsAndStopsWithoutError () { @@ -55,7 +75,8 @@ public void ContextMenu_CrashesOnRight () .WithContextMenu(ctx,menuItems) // Click in main area inside border .RightClick(1,1) - .LeftClick (2, 2) + .ScreenShot ("After open menu:",_out) + .LeftClick (3, 3) /*.Assert (Application.Top.Focused.Should ().BeAssignableTo(typeof(MenuBarItem))) .Down() .Enter()*/ From db588a9fd46b78fd62a8a26e926064ed3b696be7 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 19:10:58 +0000 Subject: [PATCH 07/12] Make colon implicit in title --- TerminalGuiFluentAssertions/Class1.cs | 2 +- Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 3d0e65cbfa..76148bfa63 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -214,7 +214,7 @@ public GuiTestContext ResizeConsole (int width, int height) } public GuiTestContext ScreenShot (string title, TextWriter writer) { - writer.WriteLine(title); + writer.WriteLine(title +":"); var text = Application.ToString (); writer.WriteLine(text); diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index 9015be5f08..a5ddec5ee5 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -75,7 +75,7 @@ public void ContextMenu_CrashesOnRight () .WithContextMenu(ctx,menuItems) // Click in main area inside border .RightClick(1,1) - .ScreenShot ("After open menu:",_out) + .ScreenShot ("After open menu",_out) .LeftClick (3, 3) /*.Assert (Application.Top.Focused.Should ().BeAssignableTo(typeof(MenuBarItem))) .Down() From 03d2388203396a2b362271c84d1e31efd1159f66 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 19:58:08 +0000 Subject: [PATCH 08/12] Add timeout to tests and fix race condition to boot --- TerminalGuiFluentAssertions/Class1.cs | 38 +++++++++++++++++-- .../FluentTests/BasicFluentAssertionTests.cs | 4 -- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs index 76148bfa63..7e0a078f40 100644 --- a/TerminalGuiFluentAssertions/Class1.cs +++ b/TerminalGuiFluentAssertions/Class1.cs @@ -8,8 +8,18 @@ namespace TerminalGuiFluentAssertions; -class FakeInput(CancellationToken hardStopToken) : IConsoleInput +class FakeInput : IConsoleInput { + private readonly CancellationToken _hardStopToken; + + private readonly CancellationTokenSource _timeoutCts; + public FakeInput (CancellationToken hardStopToken) + { + _hardStopToken = hardStopToken; + + // Create a timeout-based cancellation token too to prevent tests ever fully hanging + _timeoutCts = new (With.Timeout); + } /// public void Dispose () { } @@ -22,7 +32,7 @@ public void Dispose () { } public void Run (CancellationToken token) { // Blocks until either the token or the hardStopToken is cancelled. - WaitHandle.WaitAny (new [] { token.WaitHandle, hardStopToken.WaitHandle }); + WaitHandle.WaitAny (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle }); } } @@ -92,13 +102,18 @@ public static class With /// public static GuiTestContext A (int width, int height) where T : Toplevel, new () { - return new GuiTestContext (width,height); + return new (width,height); } + + /// + /// The global timeout to allow for any given application to run for before shutting down. + /// + public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30); } public class GuiTestContext : IDisposable where T : Toplevel, new() { private readonly CancellationTokenSource _cts = new (); - private readonly CancellationTokenSource _hardStop = new (); + private readonly CancellationTokenSource _hardStop = new (With.Timeout); private readonly Task _runTask; private Exception _ex; private readonly FakeOutput _output = new (); @@ -120,6 +135,7 @@ internal GuiTestContext (int width, int height) () => winInput, () => _output); + var booting = new SemaphoreSlim (0, 1); // Start the application in a background thread _runTask = Task.Run (() => @@ -130,6 +146,8 @@ internal GuiTestContext (int width, int height) v2.Init (null,"v2win"); + booting.Release (); + Application.Run (); // This will block, but it's on a background thread now Application.Shutdown (); @@ -146,6 +164,12 @@ internal GuiTestContext (int width, int height) } }, _cts.Token); + // Wait for booting to complete with a timeout to avoid hangs + if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result) + { + throw new TimeoutException ("Application failed to start within the allotted time."); + } + WaitIteration (); } @@ -183,6 +207,12 @@ public GuiTestContext Stop () public void Dispose () { Stop (); + + if (_hardStop.IsCancellationRequested) + { + throw new Exception ( + "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test"); + } _hardStop.Cancel(); } diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index a5ddec5ee5..080b424641 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -77,11 +77,7 @@ public void ContextMenu_CrashesOnRight () .RightClick(1,1) .ScreenShot ("After open menu",_out) .LeftClick (3, 3) - /*.Assert (Application.Top.Focused.Should ().BeAssignableTo(typeof(MenuBarItem))) - .Down() - .Enter()*/ .Stop (); Assert.True (clicked); - } } From 2aee5959d1b5abad59afc0b442dca43cf37dcc7d Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 20:03:06 +0000 Subject: [PATCH 09/12] Rename to TerminalGuiFluentTesting --- Terminal.Gui/Terminal.Gui.csproj | 2 +- Terminal.sln | 2 +- TerminalGuiFluentAssertions/Class1.cs | 404 ------------------ TerminalGuiFluentAssertions/ClassDiagram1.cd | 59 +++ TerminalGuiFluentAssertions/FakeInput.cs | 34 ++ TerminalGuiFluentAssertions/FakeNetInput.cs | 6 + TerminalGuiFluentAssertions/FakeOutput.cs | 28 ++ .../FakeWindowsInput.cs | 6 + TerminalGuiFluentAssertions/GuiTestContext.cs | 304 +++++++++++++ ...csproj => TerminalGuiFluentTesting.csproj} | 0 TerminalGuiFluentAssertions/With.cs | 22 + .../FluentTests/BasicFluentAssertionTests.cs | 2 +- Tests/UnitTests/UnitTests.csproj | 2 +- 13 files changed, 463 insertions(+), 408 deletions(-) delete mode 100644 TerminalGuiFluentAssertions/Class1.cs create mode 100644 TerminalGuiFluentAssertions/ClassDiagram1.cd create mode 100644 TerminalGuiFluentAssertions/FakeInput.cs create mode 100644 TerminalGuiFluentAssertions/FakeNetInput.cs create mode 100644 TerminalGuiFluentAssertions/FakeOutput.cs create mode 100644 TerminalGuiFluentAssertions/FakeWindowsInput.cs create mode 100644 TerminalGuiFluentAssertions/GuiTestContext.cs rename TerminalGuiFluentAssertions/{TerminalGuiFluentAssertions.csproj => TerminalGuiFluentTesting.csproj} (100%) create mode 100644 TerminalGuiFluentAssertions/With.cs diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 39b32ede9a..ef4a1a22b4 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -86,7 +86,7 @@ - + diff --git a/Terminal.sln b/Terminal.sln index c233b37ad5..65f3d7596b 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -62,7 +62,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentAssertions", "TerminalGuiFluentAssertions\TerminalGuiFluentAssertions.csproj", "{7C610F03-9E38-409F-9B21-A02D5569E16A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentAssertions\TerminalGuiFluentTesting.csproj", "{7C610F03-9E38-409F-9B21-A02D5569E16A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/TerminalGuiFluentAssertions/Class1.cs b/TerminalGuiFluentAssertions/Class1.cs deleted file mode 100644 index 7e0a078f40..0000000000 --- a/TerminalGuiFluentAssertions/Class1.cs +++ /dev/null @@ -1,404 +0,0 @@ -using System.Collections.Concurrent; -using System.Drawing; -using FluentAssertions; -using FluentAssertions.Numeric; -using Terminal.Gui; -using Terminal.Gui.ConsoleDrivers; -using static Unix.Terminal.Curses; - -namespace TerminalGuiFluentAssertions; - -class FakeInput : IConsoleInput -{ - private readonly CancellationToken _hardStopToken; - - private readonly CancellationTokenSource _timeoutCts; - public FakeInput (CancellationToken hardStopToken) - { - _hardStopToken = hardStopToken; - - // Create a timeout-based cancellation token too to prevent tests ever fully hanging - _timeoutCts = new (With.Timeout); - } - /// - public void Dispose () { } - - /// - public void Initialize (ConcurrentQueue inputBuffer) { InputBuffer = inputBuffer;} - - public ConcurrentQueue InputBuffer { get; set; } - - /// - public void Run (CancellationToken token) - { - // Blocks until either the token or the hardStopToken is cancelled. - WaitHandle.WaitAny (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle }); - } -} - -class FakeNetInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), INetInput -{ - -} - -class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), IWindowsInput -{ - -} - -class FakeOutput : IConsoleOutput -{ - public IOutputBuffer LastBuffer { get; set; } - public Size Size { get; set; } - - /// - public void Dispose () - { - - } - - /// - public void Write (ReadOnlySpan text) - { - - } - - /// - public void Write (IOutputBuffer buffer) - { - LastBuffer = buffer; - } - - - /// - public Size GetWindowSize () - { - return Size; - } - - /// - public void SetCursorVisibility (CursorVisibility visibility) - { - - } - - /// - public void SetCursorPosition (int col, int row) - { - - } - -} -/// -/// Entry point to fluent assertions. -/// -public static class With -{ - /// - /// Entrypoint to fluent assertions - /// - /// - /// - /// - public static GuiTestContext A (int width, int height) where T : Toplevel, new () - { - return new (width,height); - } - - /// - /// The global timeout to allow for any given application to run for before shutting down. - /// - public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30); -} -public class GuiTestContext : IDisposable where T : Toplevel, new() -{ - private readonly CancellationTokenSource _cts = new (); - private readonly CancellationTokenSource _hardStop = new (With.Timeout); - private readonly Task _runTask; - private Exception _ex; - private readonly FakeOutput _output = new (); - private readonly FakeWindowsInput winInput; - private View _lastView; - - internal GuiTestContext (int width, int height) - { - IApplication origApp = ApplicationImpl.Instance; - - var netInput = new FakeNetInput (_cts.Token); - winInput = new FakeWindowsInput (_cts.Token); - - _output.Size = new (width, height); - - var v2 = new ApplicationV2( - () => netInput, - ()=>_output, - () => winInput, - () => _output); - - var booting = new SemaphoreSlim (0, 1); - - // Start the application in a background thread - _runTask = Task.Run (() => - { - try - { - ApplicationImpl.ChangeInstance (v2); - - v2.Init (null,"v2win"); - - booting.Release (); - - Application.Run (); // This will block, but it's on a background thread now - - Application.Shutdown (); - } - catch (OperationCanceledException) - { } - catch (Exception ex) - { - _ex = ex; - } - finally - { - ApplicationImpl.ChangeInstance (origApp); - } - }, _cts.Token); - - // Wait for booting to complete with a timeout to avoid hangs - if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result) - { - throw new TimeoutException ("Application failed to start within the allotted time."); - } - - WaitIteration (); - } - - /// - /// Stops the application and waits for the background thread to exit. - /// - public GuiTestContext Stop () - { - if (_runTask.IsCompleted) - { - return this; - } - - Application.Invoke (()=> Application.RequestStop ()); - - // Wait for the application to stop, but give it a 1-second timeout - if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000))) - { - _cts.Cancel (); - // Timeout occurred, force the task to stop - _hardStop.Cancel (); - throw new TimeoutException ("Application failed to stop within the allotted time."); - } - _cts.Cancel (); - - if (_ex != null) - { - throw _ex; // Propagate any exception that happened in the background task - } - - return this; - } - - // Cleanup to avoid state bleed between tests - public void Dispose () - { - Stop (); - - if (_hardStop.IsCancellationRequested) - { - throw new Exception ( - "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test"); - } - _hardStop.Cancel(); - } - - /// - /// Adds the given to the current top level view - /// and performs layout. - /// - /// - /// - public GuiTestContext Add (View v) - { - WaitIteration ( - () => - { - var top = Application.Top ?? throw new Exception("Top was null so could not add view"); - top.Add (v); - top.Layout (); - _lastView = v; - }); - - return this; - } - - public GuiTestContext ResizeConsole (int width, int height) - { - _output.Size = new Size (width,height); - - return WaitIteration (); - } - public GuiTestContext ScreenShot (string title, TextWriter writer) - { - writer.WriteLine(title +":"); - var text = Application.ToString (); - - writer.WriteLine(text); - - return WaitIteration (); - } - public GuiTestContext WaitIteration (Action? a = null) - { - a ??= () => { }; - var ctsLocal = new CancellationTokenSource (); - - - Application.Invoke (()=> - { - a(); - ctsLocal.Cancel (); - }); - - // Blocks until either the token or the hardStopToken is cancelled. - WaitHandle.WaitAny (new [] - { - _cts.Token.WaitHandle, - _hardStop.Token.WaitHandle, - ctsLocal.Token.WaitHandle - }); - return this; - } - - public GuiTestContext Assert (AndConstraint be) - { - return this; - } - - public GuiTestContext RightClick (int screenX, int screenY) - { - return Click (WindowsConsole.ButtonState.Button3Pressed,screenX, screenY); - } - - public GuiTestContext LeftClick (int screenX, int screenY) - { - return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); - } - - private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY) - { - winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () - { - EventType = WindowsConsole.EventType.Mouse, - MouseEvent = new WindowsConsole.MouseEventRecord () - { - ButtonState = btn, - MousePosition = new WindowsConsole.Coord ((short)screenX, (short)screenY) - } - }); - - winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () - { - EventType = WindowsConsole.EventType.Mouse, - MouseEvent = new WindowsConsole.MouseEventRecord () - { - ButtonState = WindowsConsole.ButtonState.NoButtonPressed, - MousePosition = new WindowsConsole.Coord ((short)screenX, (short)screenY) - } - }); - - WaitIteration (); - - return this; - } - - public GuiTestContext Down () - { - winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () - { - EventType = WindowsConsole.EventType.Key, - KeyEvent = new WindowsConsole.KeyEventRecord - { - bKeyDown = true, - wRepeatCount = 0, - wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN, - wVirtualScanCode = 0, - UnicodeChar = '\0', - dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed - } - }); - - winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () - { - EventType = WindowsConsole.EventType.Key, - KeyEvent = new WindowsConsole.KeyEventRecord - { - bKeyDown = false, - wRepeatCount = 0, - wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN, - wVirtualScanCode = 0, - UnicodeChar = '\0', - dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed - } - }); - - - WaitIteration (); - - return this; - } - public GuiTestContext Enter () - { - winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () - { - EventType = WindowsConsole.EventType.Key, - KeyEvent = new WindowsConsole.KeyEventRecord - { - bKeyDown = true, - wRepeatCount = 0, - wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN, - wVirtualScanCode = 0, - UnicodeChar = '\0', - dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed - } - }); - - winInput.InputBuffer.Enqueue (new WindowsConsole.InputRecord () - { - EventType = WindowsConsole.EventType.Key, - KeyEvent = new WindowsConsole.KeyEventRecord - { - bKeyDown = false, - wRepeatCount = 0, - wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN, - wVirtualScanCode = 0, - UnicodeChar = '\0', - dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed - } - }); - - WaitIteration (); - - return this; - } - public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems) - { - LastView.MouseEvent += (s, e) => - { - if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) - { - ctx.Show (menuItems); - } - }; - - return this; - } - - public View LastView => _lastView ?? Application.Top ?? throw new Exception ("Could not determine which view to add to"); - - -} - diff --git a/TerminalGuiFluentAssertions/ClassDiagram1.cd b/TerminalGuiFluentAssertions/ClassDiagram1.cd new file mode 100644 index 0000000000..d2965ee269 --- /dev/null +++ b/TerminalGuiFluentAssertions/ClassDiagram1.cd @@ -0,0 +1,59 @@ + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAIAAAAAA= + With.cs + + + + + + AQAAAAAAACAAAQEAAAAgAAAAAAAAAAAAAAAAAAAAAAI= + FakeInput.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + FakeNetInput.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + FakeWindowsInput.cs + + + + + + + AAAAAAAAgCAAgAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA= + FakeOutput.cs + + + + + + + + + + ABJAAAIAACBAAQAAgIAAAAAgABIEgAQAIAAIBACAIgA= + GuiTestContext.cs + + + + + + + + + + \ No newline at end of file diff --git a/TerminalGuiFluentAssertions/FakeInput.cs b/TerminalGuiFluentAssertions/FakeInput.cs new file mode 100644 index 0000000000..e908443820 --- /dev/null +++ b/TerminalGuiFluentAssertions/FakeInput.cs @@ -0,0 +1,34 @@ +using System.Collections.Concurrent; +using Terminal.Gui; + +namespace TerminalGuiFluentTesting; + +internal class FakeInput : IConsoleInput +{ + private readonly CancellationToken _hardStopToken; + + private readonly CancellationTokenSource _timeoutCts; + + public FakeInput (CancellationToken hardStopToken) + { + _hardStopToken = hardStopToken; + + // Create a timeout-based cancellation token too to prevent tests ever fully hanging + _timeoutCts = new (With.Timeout); + } + + /// + public void Dispose () { } + + /// + public void Initialize (ConcurrentQueue inputBuffer) { InputBuffer = inputBuffer; } + + public ConcurrentQueue InputBuffer { get; set; } + + /// + public void Run (CancellationToken token) + { + // Blocks until either the token or the hardStopToken is cancelled. + WaitHandle.WaitAny (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle }); + } +} diff --git a/TerminalGuiFluentAssertions/FakeNetInput.cs b/TerminalGuiFluentAssertions/FakeNetInput.cs new file mode 100644 index 0000000000..6ccede4918 --- /dev/null +++ b/TerminalGuiFluentAssertions/FakeNetInput.cs @@ -0,0 +1,6 @@ +using Terminal.Gui; + +namespace TerminalGuiFluentTesting; + +internal class FakeNetInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), INetInput +{ } diff --git a/TerminalGuiFluentAssertions/FakeOutput.cs b/TerminalGuiFluentAssertions/FakeOutput.cs new file mode 100644 index 0000000000..5f2b43bce5 --- /dev/null +++ b/TerminalGuiFluentAssertions/FakeOutput.cs @@ -0,0 +1,28 @@ +using System.Drawing; +using Terminal.Gui; + +namespace TerminalGuiFluentTesting; + +internal class FakeOutput : IConsoleOutput +{ + public IOutputBuffer LastBuffer { get; set; } + public Size Size { get; set; } + + /// + public void Dispose () { } + + /// + public void Write (ReadOnlySpan text) { } + + /// + public void Write (IOutputBuffer buffer) { LastBuffer = buffer; } + + /// + public Size GetWindowSize () { return Size; } + + /// + public void SetCursorVisibility (CursorVisibility visibility) { } + + /// + public void SetCursorPosition (int col, int row) { } +} diff --git a/TerminalGuiFluentAssertions/FakeWindowsInput.cs b/TerminalGuiFluentAssertions/FakeWindowsInput.cs new file mode 100644 index 0000000000..42ccf80981 --- /dev/null +++ b/TerminalGuiFluentAssertions/FakeWindowsInput.cs @@ -0,0 +1,6 @@ +using Terminal.Gui; + +namespace TerminalGuiFluentTesting; + +internal class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput (hardStopToken), IWindowsInput +{ } diff --git a/TerminalGuiFluentAssertions/GuiTestContext.cs b/TerminalGuiFluentAssertions/GuiTestContext.cs new file mode 100644 index 0000000000..3e6570c72f --- /dev/null +++ b/TerminalGuiFluentAssertions/GuiTestContext.cs @@ -0,0 +1,304 @@ +using FluentAssertions; +using Terminal.Gui; +using Terminal.Gui.ConsoleDrivers; + +namespace TerminalGuiFluentTesting; + +public class GuiTestContext : IDisposable where T : Toplevel, new () +{ + private readonly CancellationTokenSource _cts = new (); + private readonly CancellationTokenSource _hardStop = new (With.Timeout); + private readonly Task _runTask; + private Exception _ex; + private readonly FakeOutput _output = new (); + private readonly FakeWindowsInput _winInput; + private readonly FakeNetInput _netInput; + private View _lastView; + + internal GuiTestContext (int width, int height) + { + IApplication origApp = ApplicationImpl.Instance; + + _netInput = new (_cts.Token); + _winInput = new (_cts.Token); + + _output.Size = new (width, height); + + var v2 = new ApplicationV2 ( + () => _netInput, + () => _output, + () => _winInput, + () => _output); + + var booting = new SemaphoreSlim (0, 1); + + // Start the application in a background thread + _runTask = Task.Run ( + () => + { + try + { + ApplicationImpl.ChangeInstance (v2); + + v2.Init (null, "v2win"); + + booting.Release (); + + Application.Run (); // This will block, but it's on a background thread now + + Application.Shutdown (); + } + catch (OperationCanceledException) + { } + catch (Exception ex) + { + _ex = ex; + } + finally + { + ApplicationImpl.ChangeInstance (origApp); + } + }, + _cts.Token); + + // Wait for booting to complete with a timeout to avoid hangs + if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result) + { + throw new TimeoutException ("Application failed to start within the allotted time."); + } + + WaitIteration (); + } + + /// + /// Stops the application and waits for the background thread to exit. + /// + public GuiTestContext Stop () + { + if (_runTask.IsCompleted) + { + return this; + } + + Application.Invoke (() => Application.RequestStop ()); + + // Wait for the application to stop, but give it a 1-second timeout + if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000))) + { + _cts.Cancel (); + + // Timeout occurred, force the task to stop + _hardStop.Cancel (); + + throw new TimeoutException ("Application failed to stop within the allotted time."); + } + + _cts.Cancel (); + + if (_ex != null) + { + throw _ex; // Propagate any exception that happened in the background task + } + + return this; + } + + // Cleanup to avoid state bleed between tests + public void Dispose () + { + Stop (); + + if (_hardStop.IsCancellationRequested) + { + throw new ( + "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test"); + } + + _hardStop.Cancel (); + } + + /// + /// Adds the given to the current top level view + /// and performs layout. + /// + /// + /// + public GuiTestContext Add (View v) + { + WaitIteration ( + () => + { + Toplevel top = Application.Top ?? throw new ("Top was null so could not add view"); + top.Add (v); + top.Layout (); + _lastView = v; + }); + + return this; + } + + public GuiTestContext ResizeConsole (int width, int height) + { + _output.Size = new (width, height); + + return WaitIteration (); + } + + public GuiTestContext ScreenShot (string title, TextWriter writer) + { + writer.WriteLine (title + ":"); + var text = Application.ToString (); + + writer.WriteLine (text); + + return WaitIteration (); + } + + public GuiTestContext WaitIteration (Action? a = null) + { + a ??= () => { }; + var ctsLocal = new CancellationTokenSource (); + + Application.Invoke ( + () => + { + a (); + ctsLocal.Cancel (); + }); + + // Blocks until either the token or the hardStopToken is cancelled. + WaitHandle.WaitAny ( + new [] + { + _cts.Token.WaitHandle, + _hardStop.Token.WaitHandle, + ctsLocal.Token.WaitHandle + }); + + return this; + } + + public GuiTestContext Assert (AndConstraint be) { return this; } + + public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); } + + public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); } + + private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY) + { + _winInput.InputBuffer.Enqueue ( + new() + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new() + { + ButtonState = btn, + MousePosition = new ((short)screenX, (short)screenY) + } + }); + + _winInput.InputBuffer.Enqueue ( + new() + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new() + { + ButtonState = WindowsConsole.ButtonState.NoButtonPressed, + MousePosition = new ((short)screenX, (short)screenY) + } + }); + + WaitIteration (); + + return this; + } + + public GuiTestContext Down () + { + _winInput.InputBuffer.Enqueue ( + new() + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new() + { + bKeyDown = true, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + _winInput.InputBuffer.Enqueue ( + new() + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new() + { + bKeyDown = false, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.DOWN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + WaitIteration (); + + return this; + } + + public GuiTestContext Enter () + { + _winInput.InputBuffer.Enqueue ( + new() + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new() + { + bKeyDown = true, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + _winInput.InputBuffer.Enqueue ( + new() + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new() + { + bKeyDown = false, + wRepeatCount = 0, + wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN, + wVirtualScanCode = 0, + UnicodeChar = '\0', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed + } + }); + + WaitIteration (); + + return this; + } + + public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems) + { + LastView.MouseEvent += (s, e) => + { + if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) + { + ctx.Show (menuItems); + } + }; + + return this; + } + + public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to"); +} diff --git a/TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj b/TerminalGuiFluentAssertions/TerminalGuiFluentTesting.csproj similarity index 100% rename from TerminalGuiFluentAssertions/TerminalGuiFluentAssertions.csproj rename to TerminalGuiFluentAssertions/TerminalGuiFluentTesting.csproj diff --git a/TerminalGuiFluentAssertions/With.cs b/TerminalGuiFluentAssertions/With.cs new file mode 100644 index 0000000000..73165fbf81 --- /dev/null +++ b/TerminalGuiFluentAssertions/With.cs @@ -0,0 +1,22 @@ +using Terminal.Gui; + +namespace TerminalGuiFluentTesting; + +/// +/// Entry point to fluent assertions. +/// +public static class With +{ + /// + /// Entrypoint to fluent assertions + /// + /// + /// + /// + public static GuiTestContext A (int width, int height) where T : Toplevel, new () { return new (width, height); } + + /// + /// The global timeout to allow for any given application to run for before shutting down. + /// + public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30); +} diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs index 080b424641..14f24a58e5 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; using FluentAssertions; -using TerminalGuiFluentAssertions; +using TerminalGuiFluentTesting; using Xunit.Abstractions; namespace UnitTests.FluentTests; diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 27dd519faa..9fa09fcad5 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -45,7 +45,7 @@ - + From 44e961fe393152f4d525f4635b45d016b4c70c98 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 20:13:35 +0000 Subject: [PATCH 10/12] Remove dependency on fluent assertions and fix folder paths --- Terminal.sln | 8 +------- .../ClassDiagram1.cd | 0 .../FakeInput.cs | 0 .../FakeNetInput.cs | 0 .../FakeOutput.cs | 0 .../FakeWindowsInput.cs | 0 .../GuiTestContext.cs | 8 ++++++-- .../TerminalGuiFluentTesting.csproj | 4 ---- .../With.cs | 0 .../FluentTests/BasicFluentAssertionTests.cs | 14 +++++--------- Tests/IntegrationTests/IntegrationTests.csproj | 1 + Tests/UnitTests/UnitTests.csproj | 1 - 12 files changed, 13 insertions(+), 23 deletions(-) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/ClassDiagram1.cd (100%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/FakeInput.cs (100%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/FakeNetInput.cs (100%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/FakeOutput.cs (100%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/FakeWindowsInput.cs (100%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/GuiTestContext.cs (99%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/TerminalGuiFluentTesting.csproj (79%) rename {TerminalGuiFluentAssertions => TerminalGuiFluentTesting}/With.cs (100%) rename Tests/{UnitTests => IntegrationTests}/FluentTests/BasicFluentAssertionTests.cs (86%) diff --git a/Terminal.sln b/Terminal.sln index 65f3d7596b..cddd38cad9 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32427.441 MinimumVisualStudioVersion = 10.0.40219.1 @@ -62,8 +62,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentAssertions\TerminalGuiFluentTesting.csproj", "{7C610F03-9E38-409F-9B21-A02D5569E16A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -118,10 +116,6 @@ Global {DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.Build.0 = Release|Any CPU - {7C610F03-9E38-409F-9B21-A02D5569E16A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C610F03-9E38-409F-9B21-A02D5569E16A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C610F03-9E38-409F-9B21-A02D5569E16A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C610F03-9E38-409F-9B21-A02D5569E16A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentAssertions/ClassDiagram1.cd b/TerminalGuiFluentTesting/ClassDiagram1.cd similarity index 100% rename from TerminalGuiFluentAssertions/ClassDiagram1.cd rename to TerminalGuiFluentTesting/ClassDiagram1.cd diff --git a/TerminalGuiFluentAssertions/FakeInput.cs b/TerminalGuiFluentTesting/FakeInput.cs similarity index 100% rename from TerminalGuiFluentAssertions/FakeInput.cs rename to TerminalGuiFluentTesting/FakeInput.cs diff --git a/TerminalGuiFluentAssertions/FakeNetInput.cs b/TerminalGuiFluentTesting/FakeNetInput.cs similarity index 100% rename from TerminalGuiFluentAssertions/FakeNetInput.cs rename to TerminalGuiFluentTesting/FakeNetInput.cs diff --git a/TerminalGuiFluentAssertions/FakeOutput.cs b/TerminalGuiFluentTesting/FakeOutput.cs similarity index 100% rename from TerminalGuiFluentAssertions/FakeOutput.cs rename to TerminalGuiFluentTesting/FakeOutput.cs diff --git a/TerminalGuiFluentAssertions/FakeWindowsInput.cs b/TerminalGuiFluentTesting/FakeWindowsInput.cs similarity index 100% rename from TerminalGuiFluentAssertions/FakeWindowsInput.cs rename to TerminalGuiFluentTesting/FakeWindowsInput.cs diff --git a/TerminalGuiFluentAssertions/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs similarity index 99% rename from TerminalGuiFluentAssertions/GuiTestContext.cs rename to TerminalGuiFluentTesting/GuiTestContext.cs index 3e6570c72f..c623d9762b 100644 --- a/TerminalGuiFluentAssertions/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -1,4 +1,4 @@ -using FluentAssertions; + using Terminal.Gui; using Terminal.Gui.ConsoleDrivers; @@ -178,7 +178,11 @@ public GuiTestContext WaitIteration (Action? a = null) return this; } - public GuiTestContext Assert (AndConstraint be) { return this; } + public GuiTestContext Then (Action doAction) + { + doAction (); + return this; + } public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); } diff --git a/TerminalGuiFluentAssertions/TerminalGuiFluentTesting.csproj b/TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj similarity index 79% rename from TerminalGuiFluentAssertions/TerminalGuiFluentTesting.csproj rename to TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj index 5654a72359..629e31cbed 100644 --- a/TerminalGuiFluentAssertions/TerminalGuiFluentTesting.csproj +++ b/TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj @@ -6,10 +6,6 @@ enable - - - - diff --git a/TerminalGuiFluentAssertions/With.cs b/TerminalGuiFluentTesting/With.cs similarity index 100% rename from TerminalGuiFluentAssertions/With.cs rename to TerminalGuiFluentTesting/With.cs diff --git a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs similarity index 86% rename from Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs rename to Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index 14f24a58e5..edc23a0709 100644 --- a/Tests/UnitTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; +using System.Text; +using Terminal.Gui; using TerminalGuiFluentTesting; using Xunit.Abstractions; -namespace UnitTests.FluentTests; +namespace IntegrationTests.FluentTests; public class BasicFluentAssertionTests { private readonly TextWriter _out; @@ -53,9 +49,9 @@ public void TestWindowsResize () }; using var c = With.A (40, 10) .Add (lbl ) - .Assert (lbl.Frame.Width.Should().Be(38)) // Window has 2 border + .Then (()=>Assert.Same(lbl.Frame.Width,38)) // Window has 2 border .ResizeConsole (20,20) - .Assert (lbl.Frame.Width.Should ().Be (18)) + .Then (() => Assert.Same (lbl.Frame.Width, 18)) .Stop (); } diff --git a/Tests/IntegrationTests/IntegrationTests.csproj b/Tests/IntegrationTests/IntegrationTests.csproj index 94b97b6a06..f279e21df2 100644 --- a/Tests/IntegrationTests/IntegrationTests.csproj +++ b/Tests/IntegrationTests/IntegrationTests.csproj @@ -26,6 +26,7 @@ + diff --git a/Tests/UnitTests/UnitTests.csproj b/Tests/UnitTests/UnitTests.csproj index 9fa09fcad5..fa84707494 100644 --- a/Tests/UnitTests/UnitTests.csproj +++ b/Tests/UnitTests/UnitTests.csproj @@ -45,7 +45,6 @@ - From f44175e45404c8e35c7c2eaecef0fad6cfd6551d Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 20:15:06 +0000 Subject: [PATCH 11/12] Add csproj to sln --- Terminal.sln | 9 ++++++++- TerminalGuiFluentTesting/ClassDiagram1.cd | 12 ++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Terminal.sln b/Terminal.sln index cddd38cad9..e15d8f3606 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32427.441 MinimumVisualStudioVersion = 10.0.40219.1 @@ -62,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +119,10 @@ Global {DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.Build.0 = Release|Any CPU + {2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentTesting/ClassDiagram1.cd b/TerminalGuiFluentTesting/ClassDiagram1.cd index d2965ee269..81442de091 100644 --- a/TerminalGuiFluentTesting/ClassDiagram1.cd +++ b/TerminalGuiFluentTesting/ClassDiagram1.cd @@ -8,7 +8,7 @@ - + AQAAAAAAACAAAQEAAAAgAAAAAAAAAAAAAAAAAAAAAAI= FakeInput.cs @@ -16,7 +16,7 @@ - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= FakeNetInput.cs @@ -24,7 +24,7 @@ - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= FakeWindowsInput.cs @@ -32,7 +32,7 @@ - + AAAAAAAAgCAAgAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA= FakeOutput.cs @@ -40,12 +40,12 @@ - + - ABJAAAIAACBAAQAAgIAAAAAgABIEgAQAIAAIBACAIgA= + ABJAAAIAACBAAQAAgYAAAAAgABIEgAQAIAAIBACAIgA= GuiTestContext.cs From 5e39e3ca1ff1b17a26c7544b621e1d7366e711df Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 16 Mar 2025 20:16:07 +0000 Subject: [PATCH 12/12] Fix test Same to Equal --- .../IntegrationTests/FluentTests/BasicFluentAssertionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index edc23a0709..f4e825e09e 100644 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -49,9 +49,9 @@ public void TestWindowsResize () }; using var c = With.A (40, 10) .Add (lbl ) - .Then (()=>Assert.Same(lbl.Frame.Width,38)) // Window has 2 border + .Then (()=>Assert.Equal(lbl.Frame.Width,38)) // Window has 2 border .ResizeConsole (20,20) - .Then (() => Assert.Same (lbl.Frame.Width, 18)) + .Then (() => Assert.Equal(lbl.Frame.Width, 18)) .Stop (); }