Fixes #4374 - Nukes all (?) legacy Driver and Application stuff; revamps tests (#4376)

This commit is contained in:
Tig
2025-11-11 16:29:33 -07:00
committed by GitHub
parent 559dea9239
commit d53fcd7485
310 changed files with 14827 additions and 16911 deletions

View File

@@ -3,7 +3,9 @@ using TerminalGuiFluentTesting;
namespace Terminal.Gui.Drivers;
#pragma warning disable CS1591
/// <summary>
/// Provides methods to create and manage a fake application for testing purposes.
/// </summary>
public class FakeApplicationFactory
{
/// <summary>
@@ -13,35 +15,23 @@ public class FakeApplicationFactory
/// <returns></returns>
public IDisposable SetupFakeApplication ()
{
var cts = new CancellationTokenSource ();
var fakeInput = new FakeNetInput (cts.Token);
CancellationTokenSource hardStopTokenSource = new CancellationTokenSource ();
FakeInput fakeInput = new FakeInput ();
fakeInput.ExternalCancellationTokenSource = hardStopTokenSource;
FakeOutput output = new ();
output.Size = new (80, 25);
output.SetSize (80, 25);
IApplication origApp = ApplicationImpl.Instance;
var sizeMonitor = new FakeSizeMonitor (output, output.LastBuffer!);
SizeMonitorImpl sizeMonitor = new (output);
var impl = new ApplicationImpl (new FakeNetComponentFactory (fakeInput, output, sizeMonitor));
ApplicationImpl impl = new (new FakeComponentFactory (fakeInput, output, sizeMonitor));
ApplicationImpl.ChangeInstance (impl);
// Initialize with a fake driver
impl.Init (null, "fake");
// Handle different facade types - cast to common interface instead
var d = (IConsoleDriverFacade)Application.Driver!;
sizeMonitor.SizeChanged += (_, e) =>
{
if (e.Size != null)
{
Size s = e.Size.Value;
output.Size = s;
d.OutputBuffer.SetSize (s.Width, s.Height);
}
};
return new FakeApplicationLifecycle (origApp, cts);
return new FakeApplicationLifecycle (origApp, hardStopTokenSource);
}
}

View File

@@ -1,7 +1,12 @@
#nullable enable
namespace Terminal.Gui.Drivers;
#pragma warning disable CS1591
/// <summary>
/// Implements a fake application lifecycle for testing purposes. Cleans up the application on dispose by cancelling
/// the provided <see cref="CancellationTokenSource"/> and shutting down the application.
/// </summary>
/// <param name="origApp"></param>
/// <param name="hardStop"></param>
internal class FakeApplicationLifecycle (IApplication origApp, CancellationTokenSource hardStop) : IDisposable
{
/// <inheritdoc/>

View File

@@ -1,33 +0,0 @@
using System.Collections.Concurrent;
namespace TerminalGuiFluentTesting;
internal class FakeInput<T> : IConsoleInput<T>
{
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);
}
/// <inheritdoc/>
public void Dispose () { }
/// <inheritdoc/>
public void Initialize (ConcurrentQueue<T> inputBuffer) { InputBuffer = inputBuffer; }
public ConcurrentQueue<T>? InputBuffer { get; set; }
/// <inheritdoc/>
public void Run (CancellationToken token)
{
// Blocks until either the token or the hardStopToken is cancelled.
WaitHandle.WaitAny ([token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle]);
}
}

View File

@@ -1,5 +0,0 @@

namespace TerminalGuiFluentTesting;
internal class FakeNetInput (CancellationToken hardStopToken) : FakeInput<ConsoleKeyInfo> (hardStopToken), INetInput
{ }

View File

@@ -1,38 +0,0 @@
using System.Drawing;
namespace TerminalGuiFluentTesting;
internal class FakeOutput : IConsoleOutput
{
public IOutputBuffer? LastBuffer { get; set; }
public Size Size { get; set; }
/// <inheritdoc/>
public void Dispose () { }
/// <inheritdoc/>
public void Write (ReadOnlySpan<char> text) { }
/// <inheritdoc/>
public void Write (IOutputBuffer buffer) { LastBuffer = buffer; }
/// <inheritdoc/>
public Size GetSize () { return Size; }
/// <inheritdoc/>
public void SetCursorVisibility (CursorVisibility visibility) { }
/// <inheritdoc/>
public void SetCursorPosition (int col, int row) { CursorPosition = new Point (col, row); }
/// <inheritdoc />
public void SetSize (int width, int height)
{
Size = new (width, height);
}
/// <summary>
/// The last value set by calling <see cref="SetCursorPosition"/>
/// </summary>
public Point CursorPosition { get; private set; }
}

View File

@@ -1,20 +0,0 @@
#nullable enable
using System.Drawing;
namespace Terminal.Gui.Drivers;
#pragma warning disable CS1591
public class FakeSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer _) : IConsoleSizeMonitor
{
/// <inheritdoc />
public event EventHandler<SizeChangedEventArgs>? SizeChanged;
/// <inheritdoc/>
public bool Poll () { return false; }
/// <summary>
/// Raises the <see cref="SizeChanged"/> event.
/// </summary>
/// <param name="newSize"></param>
public void RaiseSizeChanged (Size newSize) { SizeChanged?.Invoke (this, new (newSize)); }
}

View File

@@ -1,5 +0,0 @@

namespace TerminalGuiFluentTesting;
internal class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput<WindowsConsole.InputRecord> (hardStopToken), IWindowsInput
{ }

View File

@@ -0,0 +1,28 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
namespace TerminalGuiFluentTesting;
public partial class GuiTestContext
{
/// <summary>
/// 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>
public GuiTestContext WithContextMenu (PopoverMenu? contextMenu)
{
LastView.MouseEvent += (s, e) =>
{
if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
{
// Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
// and the context menu is disposed when it is closed.
Application.Popover?.Register (contextMenu);
contextMenu?.MakeVisible (e.ScreenPosition);
}
};
return this;
}
}

View File

@@ -0,0 +1,223 @@
using System.Drawing;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
namespace TerminalGuiFluentTesting;
public partial class GuiTestContext
{
/// <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.
/// </summary>
/// <param name="screenX">0 indexed screen coordinates</param>
/// <param name="screenY">0 indexed screen coordinates</param>
/// <returns></returns>
public GuiTestContext RightClick (int screenX, int screenY)
{
return EnqueueMouseEvent (new ()
{
Flags = MouseFlags.Button3Clicked,
ScreenPosition = new (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.
/// </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 EnqueueMouseEvent (new ()
{
Flags = MouseFlags.Button1Clicked,
ScreenPosition = new (screenX, screenY),
});
}
/// <summary>
/// Simulates a left mouse click on the top-left cell of the Viewport of the View of type TView determined by the
/// <paramref name="evaluator"/>.
/// </summary>
/// <typeparam name="TView"></typeparam>
/// <param name="evaluator"></param>
/// <returns></returns>
public GuiTestContext LeftClick<TView> (Func<TView, bool> evaluator) where TView : View
{
return EnqueueMouseEvent (new ()
{
Flags = MouseFlags.Button1Clicked,
}, evaluator);
}
private GuiTestContext EnqueueMouseEvent (MouseEventArgs mouseEvent)
{
// Enqueue the mouse event
WaitIteration (() =>
{
if (Application.Driver is { })
{
mouseEvent.Position = mouseEvent.ScreenPosition;
Application.Driver.InputProcessor.EnqueueMouseEvent (mouseEvent);
}
else
{
Fail ("Expected Application.Driver to be non-null.");
}
});
// Wait for the event to be processed (similar to EnqueueKeyEvent)
return WaitIteration ();
}
private GuiTestContext EnqueueMouseEvent<TView> (MouseEventArgs mouseEvent, Func<TView, bool> evaluator) where TView : View
{
var screen = Point.Empty;
GuiTestContext ctx = WaitIteration (() =>
{
TView v = Find (evaluator);
screen = v.ViewportToScreen (new Point (0, 0));
});
mouseEvent.ScreenPosition = screen;
mouseEvent.Position = new Point (0, 0);
EnqueueMouseEvent (mouseEvent);
return ctx;
}
//private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
//{
// switch (_driverType)
// {
// case TestDriver.Windows:
// _winInput!.InputQueue!.Enqueue (
// new ()
// {
// EventType = WindowsConsole.EventType.Mouse,
// MouseEvent = new ()
// {
// ButtonState = btn,
// MousePosition = new ((short)screenX, (short)screenY)
// }
// });
// _winInput.InputQueue.Enqueue (
// new ()
// {
// EventType = WindowsConsole.EventType.Mouse,
// MouseEvent = new ()
// {
// ButtonState = WindowsConsole.ButtonState.NoButtonPressed,
// MousePosition = new ((short)screenX, (short)screenY)
// }
// });
// return WaitUntil (() => _winInput.InputQueue.IsEmpty);
// case TestDriver.DotNet:
// 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 (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
// {
// SendNetKey (k, false);
// }
// return WaitIteration ();
// case TestDriver.Unix:
// int unixButton = btn switch
// {
// 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 (unixButton, screenX, screenY))
// {
// SendUnixKey (k.KeyChar, false);
// }
// return WaitIteration ();
// case TestDriver.Fake:
// int fakeButton = btn switch
// {
// 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 (fakeButton, screenX, screenY))
// {
// SendFakeKey (k, false);
// }
// return WaitIteration ();
// default:
// throw new ArgumentOutOfRangeException ();
// }
//}
/// <summary>
/// Enqueues a key down event to the current driver's input processor.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
/// <summary>
/// Enqueues a key down event to the current driver's input processor.
/// </summary>
public GuiTestContext EnqueueKeyEvent (Key key)
{
//Logging.Trace ($"Enqueuing key: {key}");
// Enqueue the key event and wait for it to be processed.
// We do this by subscribing to the Driver.KeyDown event and waiting until it is raised.
// This prevents the application from missing the key event if we enqueue it and immediately return.
bool keyReceived = false;
if (_applicationImpl?.Driver is { })
{
_applicationImpl.Driver.KeyDown += DriverOnKeyDown;
_applicationImpl.Driver.EnqueueKeyEvent (key);
WaitUntil (() => keyReceived);
}
else
{
Fail ("Expected Application.Driver to be non-null.");
}
return this;
void DriverOnKeyDown (object? sender, Key e)
{
_applicationImpl.Driver.KeyDown -= DriverOnKeyDown;
keyReceived = true;
}
}
}

View File

@@ -0,0 +1,94 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
namespace TerminalGuiFluentTesting;
public partial class GuiTestContext
{
/// <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>
/// <exception cref="ArgumentException"></exception>
public GuiTestContext Focus (View toFocus)
{
toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop);
if (!toFocus.HasFocus)
{
throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable");
}
return WaitIteration ();
}
/// <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
/// </summary>
/// <param name="evaluator">
/// Delegate that returns true if the passed View is the one
/// you are trying to focus. Leave <see langword="null"/> to focus the first view of type
/// <typeparamref name="T"/>
/// </param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public GuiTestContext Focus<T> (Func<T, bool>? evaluator = null) where T : View
{
evaluator ??= _ => true;
Toplevel? t = Application.Top;
HashSet<View> seen = new ();
if (t == null)
{
Fail ("Application.Top was null when trying to set focus");
return this;
}
do
{
View? next = t.MostFocused;
// Is view found?
if (next is T v && evaluator (v))
{
return this;
}
// No, try tab to the next (or first)
EnqueueKeyEvent (Application.NextTabKey);
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"
+ DescribeSeenViews (seen));
return this;
}
// Track the views we have seen
// We have looped around to the start again if it was already there
if (!seen.Add (next))
{
Fail (
"Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View"
+ DescribeSeenViews (seen));
return this;
}
}
while (true);
}
private string DescribeSeenViews (HashSet<View> seen) { return Environment.NewLine + string.Join (Environment.NewLine, seen); }
}

View File

@@ -0,0 +1,72 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
namespace TerminalGuiFluentTesting;
public partial class GuiTestContext
{
/// <summary>
/// Adds the given <paramref name="v"/> to the current top level view
/// and performs layout.
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
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;
}
private View? _lastView;
/// <summary>
/// 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");
private T Find<T> (Func<T, bool> evaluator) where T : View
{
Toplevel? t = Application.Top;
if (t == null)
{
Fail ("Application.Top was null when attempting to find view");
}
T? f = FindRecursive (t!, evaluator);
if (f == null)
{
Fail ("Failed to tab to a view which matched the Type and evaluator constraints in any SubViews of top");
}
return f!;
}
private T? FindRecursive<T> (View current, Func<T, bool> evaluator) where T : View
{
foreach (View subview in current.SubViews)
{
if (subview is T match && evaluator (match))
{
return match;
}
// Recursive call
T? result = FindRecursive (subview, evaluator);
if (result != null)
{
return result;
}
}
return null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TerminalGuiFluentTesting;
namespace TerminalGuiFluentTesting;
/// <summary>
/// Which driver simulation should be used for testing
/// Which driver simulation should be used for testing
/// </summary>
public enum TestDriver
{
/// <summary>
/// The Windows driver with simulation I/O but core driver classes
/// The Windows driver with simulation I/O but core driver classes
/// </summary>
Windows,
/// <summary>
/// The DotNet driver with simulation I/O but core driver classes
/// The DotNet driver with simulation I/O but core driver classes
/// </summary>
DotNet
DotNet,
/// <summary>
/// The Unix driver with simulation I/O but core driver classes
/// </summary>
Unix,
/// <summary>
/// The Fake driver that does not use any core driver classes
/// </summary>
Fake
}

View File

@@ -14,9 +14,12 @@ public static class With
/// <param name="testDriver">Which v2 testDriver to use for the test</param>
/// <param name="logWriter"></param>
/// <returns></returns>
public static GuiTestContext A<T> (int width, int height, TestDriver testDriver, TextWriter? logWriter = null) where T : Toplevel, new ()
public static GuiTestContext A<T> (int width, int height, TestDriver testDriver, TextWriter? logWriter = null) where T : Toplevel, new()
{
return new (() => new T (), width, height, testDriver, logWriter, Timeout);
return new (() => new T ()
{
//Id = $"{typeof (T).Name}"
}, width, height, testDriver, logWriter, Timeout);
}
/// <summary>
@@ -36,5 +39,5 @@ public static class With
/// </summary>
public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30);
}