Partial on #2975 - Replaces Menu v1 in many places with v2 (#4040)

* touching publish.yml

* Fixed UICatalog bugs. Added fluent tests.

* marked v1 menu stuff as obsolte

* Tweaks.
Added View.GetSubMenus<type>().

* fixed unit tests

* general messing around

* general messing around

* Playing with Fluent

* ColorScheme tweaks

* WIP: ColorScheme tweaks

* Playing with Fluent

* Merged from laptop2

* Hacky-ish fixes to:
- #4016
- #4014

* Fixed Region bug preventing menus without borders from working

* Tweaks

* Fixed a bunch of CM issues

* Fixed OoptionSelector

* ip

* FixedCM issues

* Fixed CM issues2

* Revert "FixedCM issues"

This reverts commit dd6c6a70a3.

* Reverted stuff

* Found and fixed bug in AllViews_Center_Properly

* Fixed CM issues2

* removed menuv2 onapplied.
Changed how UICatalog Applys CM

* changed test time out to see if it helkps with ubuntu fails

* reset app on fail?

* back to 1500ms

* Made StatusBar nullable.

* Code Cleanup.

* HexEditor Code Cleanup.

* HexEditor Code Cleanup.

* Back to 3000ms. Sigh.

* Trying different logic

* Trying different logic2

* Fixed potential crash in runlop

* Fixed potential crash in runlop2

* Tweaked Spinner stuff

* Removed TabView from TextEffects scenario. Not needed and possible culprit.

* back to 2000ms

* WIP: Revamping menu scenarios

* Menu Scenario refinements.
Fixed a few bugs.
Code cleanup.

* fixed unit test

* Fixed warnings

* Fixed warnings2

* Fixed File.Exit

* WIP: Dealing with QuitKey struggles

* WIP: Dealing with QuitKey struggles 2

* WIP: Dealing with QuitKey struggles 3

* Fixed ListView collection nav bug

* Fixed a bunch of menu stuff.
Fixed Appv2 stuff.

* Lots of refactoring and fixing

* Lots of unit test issues

* Fixed DebugIDisposable issues

* Fixed release build issue

* Fixed release build issue 2

* DebugIDisposable -> EnableDebugIDisposableAsserts and more

* DebugIDisposable -> EnableDebugIDisposableAsserts and more 2

* Fixed Menus scenario - context menu

* Added @bdisp suggested assert. Commented it out as it breaks tests.

* Code cleanup

* Fixed disposed but

* Fixed UICatalog exit

* Fixed Unit test I broke.
Added 'Minimal' Theme that turns off all borders etc...
This commit is contained in:
Tig
2025-04-24 05:17:58 -06:00
committed by GitHub
parent 8d3d039fb3
commit 47bcf1bf57
136 changed files with 5109 additions and 2214 deletions

View File

@@ -23,12 +23,12 @@ internal class FakeInput<T> : IConsoleInput<T>
/// <inheritdoc/>
public void Initialize (ConcurrentQueue<T> inputBuffer) { InputBuffer = inputBuffer; }
public ConcurrentQueue<T> InputBuffer { get; set; }
public ConcurrentQueue<T>? InputBuffer { get; set; }
/// <inheritdoc/>
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 });
WaitHandle.WaitAny ([token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle]);
}
}

View File

@@ -3,13 +3,12 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Terminal.Gui;
using Terminal.Gui.ConsoleDrivers;
using static Unix.Terminal.Curses;
namespace TerminalGuiFluentTesting;
/// <summary>
/// Fluent API context for testing a Terminal.Gui application. Create
/// an instance using <see cref="With"/> static class.
/// Fluent API context for testing a Terminal.Gui application. Create
/// an instance using <see cref="With"/> static class.
/// </summary>
public class GuiTestContext : IDisposable
{
@@ -23,7 +22,7 @@ public class GuiTestContext : IDisposable
private View? _lastView;
private readonly StringBuilder _logsSb;
private readonly V2TestDriver _driver;
private bool _finished=false;
private bool _finished;
internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
{
@@ -68,6 +67,7 @@ public class GuiTestContext : IDisposable
t.Closed += (s, e) => { _finished = true; };
Application.Run (t); // This will block, but it's on a background thread now
t.Dispose ();
Application.Shutdown ();
}
catch (OperationCanceledException)
@@ -97,12 +97,12 @@ public class GuiTestContext : IDisposable
private string GetDriverName ()
{
return _driver switch
{
V2TestDriver.V2Win => "v2win",
V2TestDriver.V2Net => "v2net",
_ =>
throw new ArgumentOutOfRangeException ()
};
{
V2TestDriver.V2Win => "v2win",
V2TestDriver.V2Net => "v2net",
_ =>
throw new ArgumentOutOfRangeException ()
};
}
/// <summary>
@@ -115,7 +115,7 @@ public class GuiTestContext : IDisposable
return this;
}
Application.Invoke (() => {Application.RequestStop ();});
Application.Invoke (() => { Application.RequestStop (); });
// Wait for the application to stop, but give it a 1-second timeout
if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
@@ -148,7 +148,7 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// Cleanup to avoid state bleed between tests
/// Cleanup to avoid state bleed between tests
/// </summary>
public void Dispose ()
{
@@ -184,7 +184,7 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// Simulates changing the console size e.g. by resizing window in your operating system
/// Simulates changing the console size e.g. by resizing window in your operating system
/// </summary>
/// <param name="width">new Width for the console.</param>
/// <param name="height">new Height for the console.</param>
@@ -203,11 +203,11 @@ public class GuiTestContext : IDisposable
writer.WriteLine (text);
return WaitIteration ();
return this; //WaitIteration();
}
/// <summary>
/// Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
/// Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
/// </summary>
/// <param name="writer"></param>
/// <returns></returns>
@@ -215,12 +215,12 @@ public class GuiTestContext : IDisposable
{
writer.WriteLine (_logsSb.ToString ());
return WaitIteration ();
return this; //WaitIteration();
}
/// <summary>
/// Waits until the end of the current iteration of the main loop. Optionally
/// running a given <paramref name="a"/> action on the UI thread at that time.
/// Waits until the end of the current iteration of the main loop. Optionally
/// running a given <paramref name="a"/> action on the UI thread at that time.
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
@@ -255,8 +255,8 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// Performs the supplied <paramref name="doAction"/> immediately.
/// Enables running commands without breaking the Fluent API calls.
/// Performs the supplied <paramref name="doAction"/> immediately.
/// Enables running commands without breaking the Fluent API calls.
/// </summary>
/// <param name="doAction"></param>
/// <returns></returns>
@@ -266,22 +266,20 @@ public class GuiTestContext : IDisposable
{
doAction ();
}
catch(Exception)
catch (Exception)
{
HardStop ();
throw;
}
return this;
}
/// <summary>
/// Simulates a right click at the given screen coordinates on the current driver.
/// This is a raw input event that goes through entire processing pipeline as though
/// user had pressed the mouse button physically.
/// Simulates a right click at the given screen coordinates on the current driver.
/// This is a raw input event that goes through entire processing pipeline as though
/// user had pressed the mouse button physically.
/// </summary>
/// <param name="screenX">0 indexed screen coordinates</param>
/// <param name="screenY">0 indexed screen coordinates</param>
@@ -289,29 +287,25 @@ public class GuiTestContext : IDisposable
public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); }
/// <summary>
/// Simulates a left click at the given screen coordinates on the current driver.
/// This is a raw input event that goes through entire processing pipeline as though
/// user had pressed the mouse button physically.
/// Simulates a left click at the given screen coordinates on the current driver.
/// This is a raw input event that goes through entire processing pipeline as though
/// user had pressed the mouse button physically.
/// </summary>
/// <param name="screenX">0 indexed screen coordinates</param>
/// <param name="screenY">0 indexed screen coordinates</param>
/// <returns></returns>
public GuiTestContext LeftClick (int screenX, int screenY)
{
return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY);
}
public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }
public GuiTestContext LeftClick<T> (Func<T,bool> evaluator) where T : View
{
return Click (WindowsConsole.ButtonState.Button1Pressed,evaluator);
}
public GuiTestContext LeftClick<T> (Func<T, bool> evaluator) where T : View { return Click (WindowsConsole.ButtonState.Button1Pressed, evaluator); }
private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T:View
private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T : View
{
var v = Find (evaluator);
var screen = v.ViewportToScreen (new Point (0, 0));
T v = Find (evaluator);
Point screen = v.ViewportToScreen (new Point (0, 0));
return Click (btn, screen.X, screen.Y);
}
private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
{
switch (_driver)
@@ -339,29 +333,32 @@ public class GuiTestContext : IDisposable
MousePosition = new ((short)screenX, (short)screenY)
}
});
break;
case V2TestDriver.V2Net:
int netButton = btn switch
{
WindowsConsole.ButtonState.Button1Pressed => 0,
WindowsConsole.ButtonState.Button2Pressed => 1,
WindowsConsole.ButtonState.Button3Pressed => 2,
WindowsConsole.ButtonState.RightmostButtonPressed => 2,
_ => throw new ArgumentOutOfRangeException (nameof (btn))
};
foreach (var k in NetSequences.Click (netButton, screenX, screenY))
{
WindowsConsole.ButtonState.Button1Pressed => 0,
WindowsConsole.ButtonState.Button2Pressed => 1,
WindowsConsole.ButtonState.Button3Pressed => 2,
WindowsConsole.ButtonState.RightmostButtonPressed => 2,
_ => throw new ArgumentOutOfRangeException (nameof (btn))
};
foreach (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
WaitIteration ();
return WaitIteration ();
return this;
;
}
public GuiTestContext Down ()
@@ -370,24 +367,26 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.DOWN);
WaitIteration ();
break;
case V2TestDriver.V2Net:
foreach (var k in NetSequences.Down)
foreach (ConsoleKeyInfo k in NetSequences.Down)
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
return WaitIteration ();
return this;
;
}
/// <summary>
/// Simulates the Right cursor key
/// Simulates the Right cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
@@ -397,24 +396,26 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);
WaitIteration ();
break;
case V2TestDriver.V2Net:
foreach (var k in NetSequences.Right)
foreach (ConsoleKeyInfo k in NetSequences.Right)
{
SendNetKey (k);
}
WaitIteration ();
break;
default:
throw new ArgumentOutOfRangeException ();
}
return this;
return WaitIteration ();
}
/// <summary>
/// Simulates the Left cursor key
/// Simulates the Left cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
@@ -424,23 +425,24 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.LEFT);
WaitIteration ();
break;
case V2TestDriver.V2Net:
foreach (var k in NetSequences.Left)
foreach (ConsoleKeyInfo k in NetSequences.Left)
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
return this;
return WaitIteration ();
}
/// <summary>
/// Simulates the up cursor key
/// Simulates the up cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
@@ -450,23 +452,24 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.UP);
WaitIteration ();
break;
case V2TestDriver.V2Net:
foreach (var k in NetSequences.Up)
foreach (ConsoleKeyInfo k in NetSequences.Up)
{
SendNetKey (k);
}
break;
default:
throw new ArgumentOutOfRangeException ();
}
return this;
return WaitIteration ();
}
/// <summary>
/// Simulates pressing the Return/Enter (newline) key.
/// Simulates pressing the Return/Enter (newline) key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
@@ -484,20 +487,21 @@ public class GuiTestContext : IDisposable
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
wVirtualScanCode = 28
});
break;
case V2TestDriver.V2Net:
SendNetKey (new ('\r', ConsoleKey.Enter, false, false, false));
break;
default:
throw new ArgumentOutOfRangeException ();
}
return this;
return WaitIteration ();
}
/// <summary>
/// Simulates pressing the Esc (Escape) key.
/// Simulates pressing the Esc (Escape) key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
@@ -515,12 +519,14 @@ public class GuiTestContext : IDisposable
wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE,
wVirtualScanCode = 1
});
break;
case V2TestDriver.V2Net:
// Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None
// even though you would think it would be Escape - it isn't
SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false));
break;
default:
throw new ArgumentOutOfRangeException ();
@@ -529,10 +535,8 @@ public class GuiTestContext : IDisposable
return this;
}
/// <summary>
/// Simulates pressing the Tab key.
/// Simulates pressing the Tab key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
@@ -550,12 +554,14 @@ public class GuiTestContext : IDisposable
wVirtualKeyCode = 0,
wVirtualScanCode = 0
});
break;
case V2TestDriver.V2Net:
// Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None
// even though you would think it would be Tab - it isn't
SendNetKey (new ('\t', ConsoleKey.None, false, false, false));
break;
default:
throw new ArgumentOutOfRangeException ();
@@ -565,8 +571,8 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
/// will open the supplied <paramref name="contextMenu"/>.
/// Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
/// will open the supplied <paramref name="contextMenu"/>.
/// </summary>
/// <param name="contextMenu"></param>
/// <returns></returns>
@@ -587,7 +593,7 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// The last view added (e.g. with <see cref="Add"/>) or the root/current top.
/// The last view added (e.g. with <see cref="Add"/>) or the root/current top.
/// </summary>
public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to");
@@ -620,11 +626,7 @@ public class GuiTestContext : IDisposable
WaitIteration ();
}
private void SendNetKey (ConsoleKeyInfo consoleKeyInfo)
{
_netInput.InputBuffer.Enqueue (consoleKeyInfo);
}
private void SendNetKey (ConsoleKeyInfo consoleKeyInfo) { _netInput.InputBuffer.Enqueue (consoleKeyInfo); }
/// <summary>
/// Sends a special key e.g. cursor key that does not map to a specific character
@@ -666,10 +668,23 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// Sets the input focus to the given <see cref="View"/>.
/// Throws <see cref="ArgumentException"/> if focus did not change due to system
/// constraints e.g. <paramref name="toFocus"/>
/// <see cref="View.CanFocus"/> is <see langword="false"/>
/// Sends a key to the application. This goes directly to Application and does not go through
/// a driver.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public GuiTestContext RaiseKeyDownEvent (Key key)
{
Application.RaiseKeyDownEvent (key);
return this; //WaitIteration();
}
/// <summary>
/// Sets the input focus to the given <see cref="View"/>.
/// Throws <see cref="ArgumentException"/> if focus did not change due to system
/// constraints e.g. <paramref name="toFocus"/>
/// <see cref="View.CanFocus"/> is <see langword="false"/>
/// </summary>
/// <param name="toFocus"></param>
/// <returns></returns>
@@ -687,27 +702,28 @@ public class GuiTestContext : IDisposable
}
/// <summary>
/// Tabs through the UI until a View matching the <paramref name="evaluator"/>
/// is found (of Type T) or all views are looped through (back to the beginning)
/// in which case triggers hard stop and Exception
/// Tabs through the UI until a View matching the <paramref name="evaluator"/>
/// is found (of Type T) or all views are looped through (back to the beginning)
/// in which case triggers hard stop and Exception
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public GuiTestContext Focus<T> (Func<T,bool> evaluator) where T:View
public GuiTestContext Focus<T> (Func<T, bool> evaluator) where T : View
{
var t = Application.Top;
Toplevel? t = Application.Top;
HashSet<View> seen = new ();
if (t == null)
{
Fail ("Application.Top was null when trying to set focus");
return this;
}
do
{
var next = t.MostFocused;
View? next = t.MostFocused;
// Is view found?
if (next is T v && evaluator (v))
@@ -716,13 +732,14 @@ public class GuiTestContext : IDisposable
}
// No, try tab to the next (or first)
this.Tab ();
Tab ();
WaitIteration ();
next = t.MostFocused;
if (next is null)
{
Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null");
return this;
}
@@ -734,22 +751,20 @@ public class GuiTestContext : IDisposable
return this;
}
}
while (true);
}
private T Find<T> (Func<T, bool> evaluator) where T : View
{
var t = Application.Top;
Toplevel? t = Application.Top;
if (t == null)
{
Fail ("Application.Top was null when attempting to find view");
}
var f = FindRecursive(t!, evaluator);
T? f = FindRecursive (t!, evaluator);
if (f == null)
{
@@ -761,7 +776,7 @@ public class GuiTestContext : IDisposable
private T? FindRecursive<T> (View current, Func<T, bool> evaluator) where T : View
{
foreach (var subview in current.SubViews)
foreach (View subview in current.SubViews)
{
if (subview is T match && evaluator (match))
{
@@ -769,7 +784,8 @@ public class GuiTestContext : IDisposable
}
// Recursive call
var result = FindRecursive (subview, evaluator);
T? result = FindRecursive (subview, evaluator);
if (result != null)
{
return result;
@@ -783,8 +799,7 @@ public class GuiTestContext : IDisposable
{
Stop ();
throw new Exception (reason);
throw new (reason);
}
public GuiTestContext Send (Key key)
@@ -798,6 +813,7 @@ public class GuiTestContext : IDisposable
{
Fail ("Expected Application.Driver to be IConsoleDriverFacade");
}
return this;
}
}