This commit is contained in:
tznind
2025-03-20 08:39:46 +00:00
committed by Tig
parent bacac70588
commit 266f03daef
5 changed files with 105 additions and 22 deletions

View File

@@ -5,6 +5,10 @@ using Terminal.Gui.ConsoleDrivers;
namespace TerminalGuiFluentTesting; namespace TerminalGuiFluentTesting;
/// <summary>
/// Fluent API context for testing a Terminal.Gui application. Create
/// an instance using <see cref="With"/> static class.
/// </summary>
public class GuiTestContext : IDisposable public class GuiTestContext : IDisposable
{ {
private readonly CancellationTokenSource _cts = new (); private readonly CancellationTokenSource _cts = new ();
@@ -130,7 +134,9 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
// Cleanup to avoid state bleed between tests /// <summary>
/// Cleanup to avoid state bleed between tests
/// </summary>
public void Dispose () public void Dispose ()
{ {
Stop (); Stop ();
@@ -164,6 +170,12 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// 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>
/// <returns></returns>
public GuiTestContext ResizeConsole (int width, int height) public GuiTestContext ResizeConsole (int width, int height)
{ {
_output.Size = new (width, height); _output.Size = new (width, height);
@@ -181,6 +193,11 @@ public class GuiTestContext : IDisposable
return WaitIteration (); return WaitIteration ();
} }
/// <summary>
/// Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
/// </summary>
/// <param name="writer"></param>
/// <returns></returns>
public GuiTestContext WriteOutLogs (TextWriter writer) public GuiTestContext WriteOutLogs (TextWriter writer)
{ {
writer.WriteLine (_logsSb.ToString ()); writer.WriteLine (_logsSb.ToString ());
@@ -188,6 +205,12 @@ public class GuiTestContext : IDisposable
return WaitIteration (); return 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.
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
public GuiTestContext WaitIteration (Action? a = null) public GuiTestContext WaitIteration (Action? a = null)
{ {
a ??= () => { }; a ??= () => { };
@@ -212,6 +235,12 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// Performs the supplied <paramref name="doAction"/> immediately.
/// Enables running commands without breaking the Fluent API calls.
/// </summary>
/// <param name="doAction"></param>
/// <returns></returns>
public GuiTestContext Then (Action doAction) public GuiTestContext Then (Action doAction)
{ {
doAction (); doAction ();
@@ -219,8 +248,24 @@ public class GuiTestContext : IDisposable
return this; 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.
/// </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 Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); } 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.
/// </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); }
private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY) private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
@@ -297,7 +342,11 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// Simulates the Right cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Right () public GuiTestContext Right ()
{ {
switch (_driver) switch (_driver)
@@ -319,6 +368,11 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// Simulates the Left cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Left () public GuiTestContext Left ()
{ {
switch (_driver) switch (_driver)
@@ -340,6 +394,11 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// Simulates the up cursor key
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Up () public GuiTestContext Up ()
{ {
switch (_driver) switch (_driver)
@@ -361,6 +420,11 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// Simulates pressing the Return/Enter (newline) key.
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public GuiTestContext Enter () public GuiTestContext Enter ()
{ {
switch (_driver) switch (_driver)
@@ -386,6 +450,31 @@ public class GuiTestContext : IDisposable
return this; return this;
} }
/// <summary>
/// Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
/// will open the supplied <paramref name="menuItems"/>.
/// </summary>
/// <param name="ctx"></param>
/// <param name="menuItems"></param>
/// <returns></returns>
public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems)
{
LastView.MouseEvent += (s, e) =>
{
if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
{
ctx.Show (menuItems);
}
};
return this;
}
/// <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");
/// <summary> /// <summary>
/// Send a full windows OS key including both down and up. /// Send a full windows OS key including both down and up.
/// </summary> /// </summary>
@@ -459,19 +548,4 @@ public class GuiTestContext : IDisposable
WaitIteration (); WaitIteration ();
} }
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");
} }

View File

@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -7,10 +7,17 @@ using System.Threading.Tasks;
namespace TerminalGuiFluentTesting; namespace TerminalGuiFluentTesting;
/// <summary> /// <summary>
/// Which v2 driver should be used /// Which v2 driver simulation should be used
/// </summary> /// </summary>
public enum V2TestDriver public enum V2TestDriver
{ {
/// <summary>
/// The v2 windows driver with simulation I/O but core driver classes
/// </summary>
V2Win, V2Win,
/// <summary>
/// The v2 net driver with simulation I/O but core driver classes
/// </summary>
V2Net V2Net
} }

View File

@@ -50,9 +50,10 @@ public class BasicFluentAssertionTests
using GuiTestContext c = With.A<Window> (40, 10, d) using GuiTestContext c = With.A<Window> (40, 10, d)
.Add (lbl) .Add (lbl)
.Then (() => Assert.Equal (lbl.Frame.Width, 38)) // Window has 2 border .Then (() => Assert.Equal (38, lbl.Frame.Width)) // Window has 2 border
.ResizeConsole (20, 20) .ResizeConsole (20, 20)
.Then (() => Assert.Equal (lbl.Frame.Width, 18)) .Then (() => Assert.Equal (18, lbl.Frame.Width))
.WriteOutLogs (_out)
.Stop (); .Stop ();
} }

View File

@@ -6,7 +6,7 @@ namespace UnitTests.ConsoleDrivers.V2;
public class MainLoopCoordinatorTests public class MainLoopCoordinatorTests
{ {
[Fact] [Fact]
public void TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread () public async Task TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread ()
{ {
var mockLogger = new Mock<ILogger> (); var mockLogger = new Mock<ILogger> ();
@@ -26,7 +26,7 @@ public class MainLoopCoordinatorTests
// StartAsync boots the main loop and the input thread. But if the input class bombs // StartAsync boots the main loop and the input thread. But if the input class bombs
// on startup it is important that the exception surface at the call site and not lost // on startup it is important that the exception surface at the call site and not lost
var ex = Assert.ThrowsAsync<AggregateException>(c.StartAsync).Result; var ex = await Assert.ThrowsAsync<AggregateException>(c.StartAsync);
Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message); Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message);